eslint-plugin-what 0.5.5 → 0.5.6

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 CHANGED
@@ -31,30 +31,50 @@ export default [
31
31
 
32
32
  ## Rules
33
33
 
34
+ ### `what/no-uncalled-signals`
35
+
36
+ Catches the #1 mistake for new developers: using a signal reference instead of calling it. Signals are functions -- you must call them to read the value.
37
+
38
+ ```jsx
39
+ // Bad -- renders "[Function]", conditionals always truthy
40
+ <span>{count}</span>
41
+ {isLoading && <Spinner />}
42
+ <span>{swr.data}</span>
43
+
44
+ // Good
45
+ <span>{count()}</span>
46
+ {isLoading() && <Spinner />}
47
+ <span>{swr.data()}</span>
48
+ ```
49
+
50
+ Tracks signals from `useSignal`, `signal`, `useComputed`, `computed`, and getter fields from `useSWR`, `useFetch`, `useQuery`, `useInfiniteQuery`.
51
+
34
52
  ### `what/no-signal-in-effect-deps`
35
53
 
36
54
  Prevents passing signal getters as effect dependencies. Signals are already reactive -- including them in deps arrays causes effects to re-run on every render.
37
55
 
38
56
  ```js
39
- // Bad
40
- useEffect(() => { ... }, [count()]);
57
+ // Bad -- signal reference in deps causes infinite re-runs
58
+ useEffect(() => { ... }, [count]);
41
59
 
42
- // Good
60
+ // Good -- rely on auto-tracking
43
61
  useEffect(() => { ... }, []);
44
62
  ```
45
63
 
46
64
  ### `what/reactive-jsx-children`
47
65
 
48
- Ensures dynamic values in JSX are wrapped in reactive functions so they update when signals change.
66
+ Without the What compiler, bare signal reads in JSX capture the value once and won't update. This rule ensures dynamic values are wrapped in reactive functions.
49
67
 
50
68
  ```jsx
51
- // Bad - won't update
69
+ // Bad (without compiler) -- won't update
52
70
  <p>{count()}</p>
53
71
 
54
72
  // Good
55
73
  <p>{() => count()}</p>
56
74
  ```
57
75
 
76
+ Disabled automatically in the `compiler` config preset.
77
+
58
78
  ### `what/no-signal-write-in-render`
59
79
 
60
80
  Prevents writing to signals during component render, which can cause infinite re-render loops.
@@ -75,7 +95,7 @@ function App() {
75
95
 
76
96
  ### `what/no-camelcase-events`
77
97
 
78
- Enforces lowercase event handler names (`onclick` instead of `onClick`). What Framework uses lowercase events natively.
98
+ Enforces lowercase event handler names (`onclick` instead of `onClick`). What Framework uses lowercase events natively. Disabled in the `compiler` config (the compiler normalizes events).
79
99
 
80
100
  ```jsx
81
101
  // Bad
@@ -87,7 +107,15 @@ Enforces lowercase event handler names (`onclick` instead of `onClick`). What Fr
87
107
 
88
108
  ### `what/prefer-set`
89
109
 
90
- Suggests using `signal.set()` instead of reassignment for signal updates. Off by default.
110
+ Suggests using `signal.set()` instead of `signal(value)` for signal writes. Off by default (style preference).
111
+
112
+ ```js
113
+ // Flagged
114
+ count(5);
115
+
116
+ // Preferred
117
+ count.set(5);
118
+ ```
91
119
 
92
120
  ## Config Details
93
121
 
@@ -99,6 +127,7 @@ Suggests using `signal.set()` instead of reassignment for signal updates. Off by
99
127
  'what/reactive-jsx-children': 'warn',
100
128
  'what/no-signal-write-in-render': 'warn',
101
129
  'what/no-camelcase-events': 'warn',
130
+ 'what/no-uncalled-signals': 'warn',
102
131
  'what/prefer-set': 'off',
103
132
  }
104
133
  ```
@@ -111,6 +140,7 @@ Suggests using `signal.set()` instead of reassignment for signal updates. Off by
111
140
  'what/reactive-jsx-children': 'error',
112
141
  'what/no-signal-write-in-render': 'error',
113
142
  'what/no-camelcase-events': 'error',
143
+ 'what/no-uncalled-signals': 'error',
114
144
  'what/prefer-set': 'warn',
115
145
  }
116
146
  ```
@@ -123,6 +153,7 @@ Suggests using `signal.set()` instead of reassignment for signal updates. Off by
123
153
  'what/reactive-jsx-children': 'off', // compiler handles reactive wrapping
124
154
  'what/no-signal-write-in-render': 'warn',
125
155
  'what/no-camelcase-events': 'off', // compiler normalizes events
156
+ 'what/no-uncalled-signals': 'warn',
126
157
  'what/prefer-set': 'off',
127
158
  }
128
159
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-what",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "description": "ESLint rules for What Framework — catch signal bugs, enforce patterns",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -14,11 +14,12 @@ import reactiveJsxChildren from './rules/reactive-jsx-children.js';
14
14
  import noSignalWriteInRender from './rules/no-signal-write-in-render.js';
15
15
  import noCamelcaseEvents from './rules/no-camelcase-events.js';
16
16
  import preferSet from './rules/prefer-set.js';
17
+ import noUncalledSignals from './rules/no-uncalled-signals.js';
17
18
 
18
19
  const plugin = {
19
20
  meta: {
20
21
  name: 'eslint-plugin-what',
21
- version: '0.5.2',
22
+ version: '0.5.6',
22
23
  },
23
24
 
24
25
  rules: {
@@ -27,6 +28,7 @@ const plugin = {
27
28
  'no-signal-write-in-render': noSignalWriteInRender,
28
29
  'no-camelcase-events': noCamelcaseEvents,
29
30
  'prefer-set': preferSet,
31
+ 'no-uncalled-signals': noUncalledSignals,
30
32
  },
31
33
 
32
34
  configs: {},
@@ -41,6 +43,7 @@ plugin.configs.recommended = {
41
43
  'what/reactive-jsx-children': 'warn',
42
44
  'what/no-signal-write-in-render': 'warn',
43
45
  'what/no-camelcase-events': 'warn',
46
+ 'what/no-uncalled-signals': 'warn',
44
47
  'what/prefer-set': 'off',
45
48
  },
46
49
  };
@@ -53,6 +56,7 @@ plugin.configs.strict = {
53
56
  'what/reactive-jsx-children': 'error',
54
57
  'what/no-signal-write-in-render': 'error',
55
58
  'what/no-camelcase-events': 'error',
59
+ 'what/no-uncalled-signals': 'error',
56
60
  'what/prefer-set': 'warn',
57
61
  },
58
62
  };
@@ -65,6 +69,7 @@ plugin.configs.compiler = {
65
69
  'what/reactive-jsx-children': 'off', // compiler handles reactive wrapping
66
70
  'what/no-signal-write-in-render': 'warn',
67
71
  'what/no-camelcase-events': 'off', // compiler normalizes events
72
+ 'what/no-uncalled-signals': 'warn',
68
73
  'what/prefer-set': 'off',
69
74
  },
70
75
  };
@@ -10,6 +10,8 @@
10
10
  * Good: useEffect(() => { count(); }, []) // or rely on auto-tracking
11
11
  */
12
12
 
13
+ import { createSignalTracker } from '../utils/signal-tracking.js';
14
+
13
15
  export default {
14
16
  meta: {
15
17
  type: 'problem',
@@ -26,22 +28,11 @@ export default {
26
28
  },
27
29
 
28
30
  create(context) {
29
- // Track variables initialized from signal/useSignal/computed calls
30
- const signalVars = new Set();
31
+ const tracker = createSignalTracker();
31
32
 
32
33
  return {
33
34
  VariableDeclarator(node) {
34
- if (!node.init) return;
35
-
36
- // Detect: const x = signal(...), useSignal(...), computed(...)
37
- if (
38
- node.init.type === 'CallExpression' &&
39
- node.init.callee.type === 'Identifier' &&
40
- ['signal', 'useSignal', 'computed', 'useComputed'].includes(node.init.callee.name) &&
41
- node.id.type === 'Identifier'
42
- ) {
43
- signalVars.add(node.id.name);
44
- }
35
+ tracker.visitors.VariableDeclarator(node);
45
36
  },
46
37
 
47
38
  CallExpression(node) {
@@ -60,7 +51,7 @@ export default {
60
51
  // Direct signal reference: useEffect(fn, [count])
61
52
  if (
62
53
  element.type === 'Identifier' &&
63
- signalVars.has(element.name)
54
+ tracker.isSignalLike(element.name)
64
55
  ) {
65
56
  context.report({
66
57
  node: element,
@@ -10,6 +10,8 @@
10
10
  * Good: useEffect(() => { count(0); })
11
11
  */
12
12
 
13
+ import { createSignalTracker } from '../utils/signal-tracking.js';
14
+
13
15
  export default {
14
16
  meta: {
15
17
  type: 'problem',
@@ -26,7 +28,7 @@ export default {
26
28
  },
27
29
 
28
30
  create(context) {
29
- const signalVars = new Set();
31
+ const tracker = createSignalTracker();
30
32
 
31
33
  // Track whether we're inside a "safe" write context
32
34
  function isInsideSafeContext(node) {
@@ -78,15 +80,7 @@ export default {
78
80
 
79
81
  return {
80
82
  VariableDeclarator(node) {
81
- if (!node.init) return;
82
- if (
83
- node.init.type === 'CallExpression' &&
84
- node.init.callee.type === 'Identifier' &&
85
- ['signal', 'useSignal', 'computed', 'useComputed'].includes(node.init.callee.name) &&
86
- node.id.type === 'Identifier'
87
- ) {
88
- signalVars.add(node.id.name);
89
- }
83
+ tracker.visitors.VariableDeclarator(node);
90
84
  },
91
85
 
92
86
  CallExpression(node) {
@@ -96,7 +90,7 @@ export default {
96
90
  // Direct call: count(value) — with at least one argument (0-arg is a read)
97
91
  if (
98
92
  node.callee.type === 'Identifier' &&
99
- signalVars.has(node.callee.name) &&
93
+ tracker.isSignal(node.callee.name) &&
100
94
  node.arguments.length > 0
101
95
  ) {
102
96
  signalName = node.callee.name;
@@ -106,7 +100,7 @@ export default {
106
100
  if (
107
101
  node.callee.type === 'MemberExpression' &&
108
102
  node.callee.object.type === 'Identifier' &&
109
- signalVars.has(node.callee.object.name) &&
103
+ tracker.isSignal(node.callee.object.name) &&
110
104
  node.callee.property.type === 'Identifier' &&
111
105
  node.callee.property.name === 'set'
112
106
  ) {
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Rule: what/no-uncalled-signals
3
+ *
4
+ * Catch the #1 mistake for new What Framework developers: using a signal
5
+ * reference as a value instead of calling it.
6
+ *
7
+ * Signals are functions — you must call them to read the value.
8
+ * Using a signal without () gives you the function reference, which:
9
+ * - Renders as "[Function]" in JSX
10
+ * - Is always truthy in conditionals
11
+ * - Produces wrong comparisons
12
+ *
13
+ * Bad: <span>{count}</span> → renders "[Function]"
14
+ * Bad: {isLoading && <Spinner />} → always truthy
15
+ * Bad: {swr.data} → renders "[Function]"
16
+ * Bad: `Total: ${count}` → "[Function]"
17
+ *
18
+ * Good: <span>{count()}</span>
19
+ * Good: {isLoading() && <Spinner />}
20
+ * Good: {swr.data()}
21
+ * Good: `Total: ${count()}`
22
+ *
23
+ * Does NOT warn when:
24
+ * - Signal is passed as a callback argument: fn(count)
25
+ * - Signal method is accessed: count.set(5), count.peek()
26
+ * - Signal is on left side of assignment or in typeof
27
+ * - Signal is used in an event handler attribute value: onClick={handler}
28
+ */
29
+
30
+ import { createSignalTracker, SIGNAL_METHODS } from '../utils/signal-tracking.js';
31
+
32
+ export default {
33
+ meta: {
34
+ type: 'problem',
35
+ docs: {
36
+ description: 'Require calling signals to read their value — catch missing ()',
37
+ recommended: true,
38
+ },
39
+ messages: {
40
+ uncalledSignal:
41
+ '"{{name}}" is a signal — call it to read the value: {{name}}()',
42
+ uncalledSWRField:
43
+ '"{{obj}}.{{prop}}" is a signal getter — call it to read the value: {{obj}}.{{prop}}()',
44
+ uncalledDestructuredGetter:
45
+ '"{{name}}" is a signal getter from a data hook — call it to read the value: {{name}}()',
46
+ },
47
+ schema: [],
48
+ },
49
+
50
+ create(context) {
51
+ const tracker = createSignalTracker();
52
+
53
+ /**
54
+ * Check if a node is in a "pass-through" position where the signal
55
+ * reference itself is intentionally used (not read for its value).
56
+ */
57
+ function isPassThrough(node) {
58
+ const parent = node.parent;
59
+ if (!parent) return false;
60
+
61
+ // Argument to a function call: someFunc(count) — passing the signal
62
+ // EXCEPT: we still warn in JSX expression containers and template literals
63
+ if (parent.type === 'CallExpression' && parent.arguments.includes(node)) {
64
+ // Check if the call is a known signal method on this very signal
65
+ // e.g., count.set(otherSignal) — otherSignal is an arg, that's fine
66
+ return true;
67
+ }
68
+
69
+ // Property value in object: { handler: count } — passing reference
70
+ if (parent.type === 'Property' && parent.value === node) {
71
+ // Unless it's a JSX spread or data object where value is expected
72
+ return true;
73
+ }
74
+
75
+ // Array element: [count, other] — building a collection of signals
76
+ if (parent.type === 'ArrayExpression') return true;
77
+
78
+ // Assignment: someVar = count — storing the signal
79
+ if (parent.type === 'AssignmentExpression' && parent.right === node) return true;
80
+
81
+ // Variable init: const x = count — aliasing the signal
82
+ if (parent.type === 'VariableDeclarator' && parent.init === node) return true;
83
+
84
+ // Return statement: return count — returning signal from function
85
+ if (parent.type === 'ReturnStatement') return true;
86
+
87
+ // typeof check
88
+ if (parent.type === 'UnaryExpression' && parent.operator === 'typeof') return true;
89
+
90
+ // Conditional (ternary) test position is NOT pass-through — we want to warn
91
+ // Logical expression operand is NOT pass-through — we want to warn
92
+
93
+ return false;
94
+ }
95
+
96
+ /**
97
+ * Check if a node is a member expression accessing a safe signal method.
98
+ * e.g., count.set, count.peek, count.subscribe
99
+ */
100
+ function isMethodAccess(node) {
101
+ const parent = node.parent;
102
+ return (
103
+ parent?.type === 'MemberExpression' &&
104
+ parent.object === node &&
105
+ parent.property?.type === 'Identifier' &&
106
+ SIGNAL_METHODS.has(parent.property.name)
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Check if node is the callee of a call expression (i.e., it IS being called).
112
+ */
113
+ function isBeingCalled(node) {
114
+ return node.parent?.type === 'CallExpression' && node.parent.callee === node;
115
+ }
116
+
117
+ /**
118
+ * Check if a MemberExpression is the callee of a call (being called).
119
+ */
120
+ function isMemberBeingCalled(node) {
121
+ return node.parent?.type === 'CallExpression' && node.parent.callee === node;
122
+ }
123
+
124
+ /**
125
+ * Check if node is a JSX attribute value (event handler).
126
+ * e.g., onClick={handler} — not a value context.
127
+ */
128
+ function isJSXAttributeValue(node) {
129
+ // Walk up: Identifier -> JSXExpressionContainer -> JSXAttribute
130
+ const exprContainer = node.parent;
131
+ if (exprContainer?.type === 'JSXExpressionContainer') {
132
+ return exprContainer.parent?.type === 'JSXAttribute';
133
+ }
134
+ return false;
135
+ }
136
+
137
+ function checkIdentifier(node) {
138
+ const name = node.name;
139
+
140
+ // Skip if being called: count() — correct usage
141
+ if (isBeingCalled(node)) return;
142
+
143
+ // Skip method access: count.set(...), count.peek()
144
+ if (isMethodAccess(node)) return;
145
+
146
+ // Skip pass-through positions
147
+ if (isPassThrough(node)) return;
148
+
149
+ // Skip JSX attribute values (event handlers): onClick={handler}
150
+ if (isJSXAttributeValue(node)) return;
151
+
152
+ // Check: is this a direct signal variable?
153
+ if (tracker.isSignalLike(name)) {
154
+ context.report({
155
+ node,
156
+ messageId: tracker.isSignal(name) ? 'uncalledSignal' : 'uncalledDestructuredGetter',
157
+ data: { name },
158
+ });
159
+ }
160
+ }
161
+
162
+ function checkMemberExpression(node) {
163
+ // Only check: swr.data, swr.error, swr.isLoading, etc.
164
+ if (
165
+ node.object.type === 'Identifier' &&
166
+ tracker.isSWRObject(node.object.name) &&
167
+ node.property.type === 'Identifier' &&
168
+ tracker.isSWRGetterField(node.property.name)
169
+ ) {
170
+ // Skip if being called: swr.data() — correct usage
171
+ if (isMemberBeingCalled(node)) return;
172
+
173
+ // Skip if further member access: swr.data.something (unusual but possible)
174
+ if (node.parent?.type === 'MemberExpression' && node.parent.object === node) return;
175
+
176
+ // Skip pass-through positions on the parent
177
+ if (isPassThrough(node)) return;
178
+
179
+ context.report({
180
+ node,
181
+ messageId: 'uncalledSWRField',
182
+ data: {
183
+ obj: node.object.name,
184
+ prop: node.property.name,
185
+ },
186
+ });
187
+ }
188
+ }
189
+
190
+ return {
191
+ // Merge the signal tracker's visitors
192
+ VariableDeclarator(node) {
193
+ tracker.visitors.VariableDeclarator(node);
194
+ },
195
+
196
+ // Check bare identifiers
197
+ Identifier(node) {
198
+ // Skip declaration positions (variable names, function params, etc.)
199
+ const parent = node.parent;
200
+ if (!parent) return;
201
+
202
+ // Skip: const count = ..., function count() {}, { count: ... } key
203
+ if (parent.type === 'VariableDeclarator' && parent.id === node) return;
204
+ if (parent.type === 'FunctionDeclaration' && parent.id === node) return;
205
+ if (parent.type === 'Property' && parent.key === node && !parent.computed) return;
206
+ if (parent.type === 'ImportSpecifier') return;
207
+ if (parent.type === 'ImportDefaultSpecifier') return;
208
+
209
+ // Skip: member expression property (count.set — we handle this separately)
210
+ if (parent.type === 'MemberExpression' && parent.property === node && !parent.computed) return;
211
+
212
+ checkIdentifier(node);
213
+ },
214
+
215
+ // Check member expressions like swr.data
216
+ MemberExpression(node) {
217
+ checkMemberExpression(node);
218
+ },
219
+ };
220
+ },
221
+ };
@@ -13,6 +13,8 @@
13
13
  * This rule is off by default (style preference, not a bug).
14
14
  */
15
15
 
16
+ import { createSignalTracker } from '../utils/signal-tracking.js';
17
+
16
18
  export default {
17
19
  meta: {
18
20
  type: 'suggestion',
@@ -29,26 +31,18 @@ export default {
29
31
  },
30
32
 
31
33
  create(context) {
32
- const signalVars = new Set();
34
+ const tracker = createSignalTracker();
33
35
 
34
36
  return {
35
37
  VariableDeclarator(node) {
36
- if (!node.init) return;
37
- if (
38
- node.init.type === 'CallExpression' &&
39
- node.init.callee.type === 'Identifier' &&
40
- ['signal', 'useSignal', 'computed', 'useComputed'].includes(node.init.callee.name) &&
41
- node.id.type === 'Identifier'
42
- ) {
43
- signalVars.add(node.id.name);
44
- }
38
+ tracker.visitors.VariableDeclarator(node);
45
39
  },
46
40
 
47
41
  CallExpression(node) {
48
42
  // Only match: signalVar(value) with exactly 1 argument
49
43
  if (
50
44
  node.callee.type !== 'Identifier' ||
51
- !signalVars.has(node.callee.name) ||
45
+ !tracker.isSignal(node.callee.name) ||
52
46
  node.arguments.length !== 1
53
47
  ) return;
54
48
 
@@ -2,7 +2,7 @@
2
2
  * Rule: what/reactive-jsx-children
3
3
  *
4
4
  * Warn when using bare signal calls as JSX children without the compiler.
5
- * Without the What compiler, esbuild/TS handles JSX h() calls, and a bare
5
+ * Without the What compiler, esbuild/TS handles JSX -> h() calls, and a bare
6
6
  * signal read like {count()} won't be reactive — it captures the value once.
7
7
  *
8
8
  * The rule checks if the project uses what-compiler (via config option or
@@ -13,6 +13,8 @@
13
13
  * Good (without compiler): <p>{() => count()}</p>
14
14
  */
15
15
 
16
+ import { createSignalTracker } from '../utils/signal-tracking.js';
17
+
16
18
  export default {
17
19
  meta: {
18
20
  type: 'problem',
@@ -45,20 +47,11 @@ export default {
45
47
  // If the user explicitly says they have the compiler, skip all checks
46
48
  if (options.hasCompiler === true) return {};
47
49
 
48
- // Track signal variables
49
- const signalVars = new Set();
50
+ const tracker = createSignalTracker();
50
51
 
51
52
  return {
52
53
  VariableDeclarator(node) {
53
- if (!node.init) return;
54
- if (
55
- node.init.type === 'CallExpression' &&
56
- node.init.callee.type === 'Identifier' &&
57
- ['signal', 'useSignal', 'computed', 'useComputed'].includes(node.init.callee.name) &&
58
- node.id.type === 'Identifier'
59
- ) {
60
- signalVars.add(node.id.name);
61
- }
54
+ tracker.visitors.VariableDeclarator(node);
62
55
  },
63
56
 
64
57
  // JSX expression: {count()}
@@ -76,7 +69,7 @@ export default {
76
69
  if (
77
70
  expr.type === 'CallExpression' &&
78
71
  expr.callee.type === 'Identifier' &&
79
- signalVars.has(expr.callee.name) &&
72
+ tracker.isSignalLike(expr.callee.name) &&
80
73
  expr.arguments.length === 0
81
74
  ) {
82
75
  context.report({
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Shared signal-tracking utility for eslint-plugin-what rules.
3
+ *
4
+ * Single source of truth for identifying signal variables across all rules.
5
+ * Tracks variables from useSignal, signal, useComputed, computed,
6
+ * useSWR destructured fields, useFetch, useQuery, useInfiniteQuery, and createResource.
7
+ */
8
+
9
+ // Functions that return a signal or computed directly
10
+ export const SIGNAL_CREATORS = [
11
+ 'signal', 'useSignal', 'computed', 'useComputed', 'memo',
12
+ ];
13
+
14
+ // Functions whose return value has signal-like getter properties
15
+ export const SWR_LIKE_HOOKS = [
16
+ 'useSWR', 'useFetch', 'useQuery', 'useInfiniteQuery',
17
+ ];
18
+
19
+ // Properties on SWR-like return objects that are getter functions (must be called)
20
+ export const SWR_GETTER_FIELDS = new Set([
21
+ 'data', 'error', 'isLoading', 'isValidating',
22
+ 'isFetching', 'isError', 'isSuccess',
23
+ 'status', 'fetchStatus',
24
+ 'hasNextPage', 'hasPreviousPage',
25
+ 'isFetchingNextPage', 'isFetchingPreviousPage',
26
+ ]);
27
+
28
+ // Properties on signals/computeds that are safe to access without calling
29
+ export const SIGNAL_METHODS = new Set([
30
+ 'set', 'peek', 'subscribe', '_signal', '_subs', '_debugName',
31
+ ]);
32
+
33
+ /**
34
+ * Creates a signal tracker for use in ESLint rule visitors.
35
+ *
36
+ * Returns an object with:
37
+ * - visitors: AST visitors to merge into the rule's return
38
+ * - isSignal(name): check if a variable is a known signal
39
+ * - isSWRGetter(objectName, propName): check if obj.prop is a SWR getter field
40
+ * - swrObjects: Set of variable names that hold SWR-like return objects
41
+ */
42
+ export function createSignalTracker() {
43
+ const signalVars = new Set();
44
+ const swrObjects = new Set();
45
+ // Track SWR destructured fields: { data, error, isLoading } = useSWR(...)
46
+ const swrGetterVars = new Set();
47
+
48
+ const visitors = {
49
+ VariableDeclarator(node) {
50
+ if (!node.init) return;
51
+
52
+ // Direct signal creation: const x = useSignal(0)
53
+ if (
54
+ node.init.type === 'CallExpression' &&
55
+ node.init.callee.type === 'Identifier' &&
56
+ SIGNAL_CREATORS.includes(node.init.callee.name) &&
57
+ node.id.type === 'Identifier'
58
+ ) {
59
+ signalVars.add(node.id.name);
60
+ }
61
+
62
+ // SWR-like hook call
63
+ if (
64
+ node.init.type === 'CallExpression' &&
65
+ node.init.callee.type === 'Identifier' &&
66
+ SWR_LIKE_HOOKS.includes(node.init.callee.name)
67
+ ) {
68
+ // const swr = useSWR(...) — track the whole object
69
+ if (node.id.type === 'Identifier') {
70
+ swrObjects.add(node.id.name);
71
+ }
72
+
73
+ // const { data, error, isLoading } = useSWR(...)
74
+ if (node.id.type === 'ObjectPattern') {
75
+ for (const prop of node.id.properties) {
76
+ if (
77
+ prop.type === 'Property' &&
78
+ prop.key.type === 'Identifier' &&
79
+ SWR_GETTER_FIELDS.has(prop.key.name) &&
80
+ prop.value.type === 'Identifier'
81
+ ) {
82
+ swrGetterVars.add(prop.value.name);
83
+ }
84
+ }
85
+ }
86
+ }
87
+
88
+ // createResource returns [dataSignal, { loading, error, ... }]
89
+ if (
90
+ node.init.type === 'CallExpression' &&
91
+ node.init.callee.type === 'Identifier' &&
92
+ node.init.callee.name === 'createResource' &&
93
+ node.id.type === 'ArrayPattern'
94
+ ) {
95
+ const elements = node.id.elements;
96
+ // First element is a signal
97
+ if (elements[0]?.type === 'Identifier') {
98
+ signalVars.add(elements[0].name);
99
+ }
100
+ }
101
+ },
102
+ };
103
+
104
+ return {
105
+ visitors,
106
+ isSignal: (name) => signalVars.has(name),
107
+ isSignalLike: (name) => signalVars.has(name) || swrGetterVars.has(name),
108
+ isSWRObject: (name) => swrObjects.has(name),
109
+ isSWRGetterField: (propName) => SWR_GETTER_FIELDS.has(propName),
110
+ signalVars,
111
+ swrObjects,
112
+ swrGetterVars,
113
+ };
114
+ }