eslint-plugin-what 0.5.4 → 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 +39 -8
- package/package.json +4 -4
- package/src/index.js +6 -1
- package/src/rules/no-signal-in-effect-deps.js +5 -14
- package/src/rules/no-signal-write-in-render.js +6 -12
- package/src/rules/no-uncalled-signals.js +221 -0
- package/src/rules/prefer-set.js +5 -11
- package/src/rules/reactive-jsx-children.js +6 -13
- package/src/utils/signal-tracking.js +114 -0
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
|
-
|
|
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
|
|
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
|
|
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
|
```
|
|
@@ -130,7 +161,7 @@ Suggests using `signal.set()` instead of reassignment for signal updates. Off by
|
|
|
130
161
|
## Links
|
|
131
162
|
|
|
132
163
|
- [Documentation](https://whatfw.com)
|
|
133
|
-
- [GitHub](https://github.com/
|
|
164
|
+
- [GitHub](https://github.com/CelsianJs/whatfw)
|
|
134
165
|
|
|
135
166
|
## License
|
|
136
167
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-what",
|
|
3
|
-
"version": "0.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",
|
|
@@ -21,14 +21,14 @@
|
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"eslint": ">=9.0.0"
|
|
23
23
|
},
|
|
24
|
-
"author": "",
|
|
24
|
+
"author": "ZVN DEV (https://zvndev.com)",
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"repository": {
|
|
27
27
|
"type": "git",
|
|
28
|
-
"url": "https://github.com/
|
|
28
|
+
"url": "https://github.com/CelsianJs/whatfw"
|
|
29
29
|
},
|
|
30
30
|
"bugs": {
|
|
31
|
-
"url": "https://github.com/
|
|
31
|
+
"url": "https://github.com/CelsianJs/whatfw/issues"
|
|
32
32
|
},
|
|
33
33
|
"homepage": "https://whatfw.com"
|
|
34
34
|
}
|
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.
|
|
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
|
-
|
|
30
|
-
const signalVars = new Set();
|
|
31
|
+
const tracker = createSignalTracker();
|
|
31
32
|
|
|
32
33
|
return {
|
|
33
34
|
VariableDeclarator(node) {
|
|
34
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|
package/src/rules/prefer-set.js
CHANGED
|
@@ -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
|
|
34
|
+
const tracker = createSignalTracker();
|
|
33
35
|
|
|
34
36
|
return {
|
|
35
37
|
VariableDeclarator(node) {
|
|
36
|
-
|
|
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
|
-
!
|
|
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
|
|
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
|
-
|
|
49
|
-
const signalVars = new Set();
|
|
50
|
+
const tracker = createSignalTracker();
|
|
50
51
|
|
|
51
52
|
return {
|
|
52
53
|
VariableDeclarator(node) {
|
|
53
|
-
|
|
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
|
-
|
|
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
|
+
}
|