eslint-plugin-what 0.5.5 → 0.6.0
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 +3 -3
- package/src/index.js +21 -1
- package/src/rules/no-h-in-user-code.js +76 -0
- package/src/rules/no-set-in-computed.js +154 -0
- 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/rules/signal-call-in-jsx.js +128 -0
- 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/CelsianJs/
|
|
164
|
+
- [GitHub](https://github.com/CelsianJs/what-framework)
|
|
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.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "ESLint rules for What Framework — catch signal bugs, enforce patterns",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"repository": {
|
|
27
27
|
"type": "git",
|
|
28
|
-
"url": "https://github.com/CelsianJs/
|
|
28
|
+
"url": "https://github.com/CelsianJs/what-framework"
|
|
29
29
|
},
|
|
30
30
|
"bugs": {
|
|
31
|
-
"url": "https://github.com/CelsianJs/
|
|
31
|
+
"url": "https://github.com/CelsianJs/what-framework/issues"
|
|
32
32
|
},
|
|
33
33
|
"homepage": "https://whatfw.com"
|
|
34
34
|
}
|
package/src/index.js
CHANGED
|
@@ -14,11 +14,15 @@ 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';
|
|
18
|
+
import noHInUserCode from './rules/no-h-in-user-code.js';
|
|
19
|
+
import signalCallInJsx from './rules/signal-call-in-jsx.js';
|
|
20
|
+
import noSetInComputed from './rules/no-set-in-computed.js';
|
|
17
21
|
|
|
18
22
|
const plugin = {
|
|
19
23
|
meta: {
|
|
20
24
|
name: 'eslint-plugin-what',
|
|
21
|
-
version: '0.
|
|
25
|
+
version: '0.6.0',
|
|
22
26
|
},
|
|
23
27
|
|
|
24
28
|
rules: {
|
|
@@ -27,6 +31,10 @@ const plugin = {
|
|
|
27
31
|
'no-signal-write-in-render': noSignalWriteInRender,
|
|
28
32
|
'no-camelcase-events': noCamelcaseEvents,
|
|
29
33
|
'prefer-set': preferSet,
|
|
34
|
+
'no-uncalled-signals': noUncalledSignals,
|
|
35
|
+
'no-h-in-user-code': noHInUserCode,
|
|
36
|
+
'signal-call-in-jsx': signalCallInJsx,
|
|
37
|
+
'no-set-in-computed': noSetInComputed,
|
|
30
38
|
},
|
|
31
39
|
|
|
32
40
|
configs: {},
|
|
@@ -41,7 +49,11 @@ plugin.configs.recommended = {
|
|
|
41
49
|
'what/reactive-jsx-children': 'warn',
|
|
42
50
|
'what/no-signal-write-in-render': 'warn',
|
|
43
51
|
'what/no-camelcase-events': 'warn',
|
|
52
|
+
'what/no-uncalled-signals': 'warn',
|
|
44
53
|
'what/prefer-set': 'off',
|
|
54
|
+
'what/no-h-in-user-code': 'warn',
|
|
55
|
+
'what/signal-call-in-jsx': 'warn',
|
|
56
|
+
'what/no-set-in-computed': 'error',
|
|
45
57
|
},
|
|
46
58
|
};
|
|
47
59
|
|
|
@@ -53,7 +65,11 @@ plugin.configs.strict = {
|
|
|
53
65
|
'what/reactive-jsx-children': 'error',
|
|
54
66
|
'what/no-signal-write-in-render': 'error',
|
|
55
67
|
'what/no-camelcase-events': 'error',
|
|
68
|
+
'what/no-uncalled-signals': 'error',
|
|
56
69
|
'what/prefer-set': 'warn',
|
|
70
|
+
'what/no-h-in-user-code': 'error',
|
|
71
|
+
'what/signal-call-in-jsx': 'error',
|
|
72
|
+
'what/no-set-in-computed': 'error',
|
|
57
73
|
},
|
|
58
74
|
};
|
|
59
75
|
|
|
@@ -65,7 +81,11 @@ plugin.configs.compiler = {
|
|
|
65
81
|
'what/reactive-jsx-children': 'off', // compiler handles reactive wrapping
|
|
66
82
|
'what/no-signal-write-in-render': 'warn',
|
|
67
83
|
'what/no-camelcase-events': 'off', // compiler normalizes events
|
|
84
|
+
'what/no-uncalled-signals': 'warn',
|
|
68
85
|
'what/prefer-set': 'off',
|
|
86
|
+
'what/no-h-in-user-code': 'warn',
|
|
87
|
+
'what/signal-call-in-jsx': 'off', // compiler handles signal wrapping in JSX
|
|
88
|
+
'what/no-set-in-computed': 'error',
|
|
69
89
|
},
|
|
70
90
|
};
|
|
71
91
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: what/no-h-in-user-code
|
|
3
|
+
*
|
|
4
|
+
* Warn when user code imports `h` from what-framework.
|
|
5
|
+
* Users should use JSX syntax instead of calling h() directly.
|
|
6
|
+
* The compiler handles JSX-to-h() transformation automatically.
|
|
7
|
+
*
|
|
8
|
+
* Bad: import { h } from 'what-framework';
|
|
9
|
+
* h('div', { class: 'foo' }, 'Hello');
|
|
10
|
+
*
|
|
11
|
+
* Good: <div class="foo">Hello</div>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
meta: {
|
|
16
|
+
type: 'suggestion',
|
|
17
|
+
docs: {
|
|
18
|
+
description: 'Disallow importing h() from what-framework — use JSX instead',
|
|
19
|
+
recommended: true,
|
|
20
|
+
},
|
|
21
|
+
messages: {
|
|
22
|
+
noHImport:
|
|
23
|
+
'Avoid importing "h" directly. Use JSX syntax instead — ' +
|
|
24
|
+
'the What compiler transforms JSX to optimized template() + insert() calls automatically.',
|
|
25
|
+
noHCall:
|
|
26
|
+
'Avoid calling h() directly in user code. Use JSX syntax instead — ' +
|
|
27
|
+
'the What compiler transforms JSX to optimized template() + insert() calls automatically.',
|
|
28
|
+
},
|
|
29
|
+
schema: [],
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
create(context) {
|
|
33
|
+
let hImported = false;
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
ImportDeclaration(node) {
|
|
37
|
+
// Check for imports from what-framework or what-core
|
|
38
|
+
const source = node.source.value;
|
|
39
|
+
if (
|
|
40
|
+
source === 'what-framework' ||
|
|
41
|
+
source === 'what-core' ||
|
|
42
|
+
source === 'what-framework/h' ||
|
|
43
|
+
source === 'what-core/h'
|
|
44
|
+
) {
|
|
45
|
+
for (const spec of node.specifiers) {
|
|
46
|
+
if (
|
|
47
|
+
spec.type === 'ImportSpecifier' &&
|
|
48
|
+
spec.imported.type === 'Identifier' &&
|
|
49
|
+
spec.imported.name === 'h'
|
|
50
|
+
) {
|
|
51
|
+
hImported = true;
|
|
52
|
+
context.report({
|
|
53
|
+
node: spec,
|
|
54
|
+
messageId: 'noHImport',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
CallExpression(node) {
|
|
62
|
+
// Only warn on h() calls if h was imported from the framework
|
|
63
|
+
if (
|
|
64
|
+
hImported &&
|
|
65
|
+
node.callee.type === 'Identifier' &&
|
|
66
|
+
node.callee.name === 'h'
|
|
67
|
+
) {
|
|
68
|
+
context.report({
|
|
69
|
+
node,
|
|
70
|
+
messageId: 'noHCall',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: what/no-set-in-computed
|
|
3
|
+
*
|
|
4
|
+
* Error when .set() is called inside a computed() callback.
|
|
5
|
+
* Writing to signals inside computed() can cause infinite loops because
|
|
6
|
+
* the write triggers re-evaluation of the computed which writes again.
|
|
7
|
+
*
|
|
8
|
+
* Bad:
|
|
9
|
+
* const doubled = computed(() => {
|
|
10
|
+
* otherSignal.set(count() * 2); // writes inside computed
|
|
11
|
+
* return count() * 2;
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* Good:
|
|
15
|
+
* const doubled = computed(() => count() * 2);
|
|
16
|
+
* effect(() => otherSignal.set(doubled()));
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createSignalTracker } from '../utils/signal-tracking.js';
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
meta: {
|
|
23
|
+
type: 'problem',
|
|
24
|
+
docs: {
|
|
25
|
+
description: 'Disallow signal writes (.set()) inside computed() callbacks',
|
|
26
|
+
recommended: true,
|
|
27
|
+
},
|
|
28
|
+
messages: {
|
|
29
|
+
setInComputed:
|
|
30
|
+
'Signal.set() called inside a computed() callback. ' +
|
|
31
|
+
'This may cause infinite loops. Move signal writes to effect() instead.',
|
|
32
|
+
signalWriteInComputed:
|
|
33
|
+
'Signal write to "{{name}}" inside a computed() callback. ' +
|
|
34
|
+
'This may cause infinite loops. Move signal writes to effect() instead.',
|
|
35
|
+
},
|
|
36
|
+
schema: [],
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
create(context) {
|
|
40
|
+
const tracker = createSignalTracker();
|
|
41
|
+
// Stack of computed scopes — tracks whether we're inside a computed callback
|
|
42
|
+
let computedDepth = 0;
|
|
43
|
+
|
|
44
|
+
function isInsideComputed() {
|
|
45
|
+
return computedDepth > 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
VariableDeclarator(node) {
|
|
50
|
+
tracker.visitors.VariableDeclarator(node);
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
CallExpression(node) {
|
|
54
|
+
// Track entering computed() or useComputed() callbacks
|
|
55
|
+
if (
|
|
56
|
+
node.callee.type === 'Identifier' &&
|
|
57
|
+
(node.callee.name === 'computed' || node.callee.name === 'useComputed') &&
|
|
58
|
+
node.arguments.length > 0 &&
|
|
59
|
+
(node.arguments[0].type === 'ArrowFunctionExpression' ||
|
|
60
|
+
node.arguments[0].type === 'FunctionExpression')
|
|
61
|
+
) {
|
|
62
|
+
// We'll check the body in the function visitor below
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check for .set() calls inside computed
|
|
66
|
+
if (isInsideComputed()) {
|
|
67
|
+
// signal.set(value)
|
|
68
|
+
if (
|
|
69
|
+
node.callee.type === 'MemberExpression' &&
|
|
70
|
+
node.callee.property.type === 'Identifier' &&
|
|
71
|
+
node.callee.property.name === 'set'
|
|
72
|
+
) {
|
|
73
|
+
const objName = node.callee.object.type === 'Identifier'
|
|
74
|
+
? node.callee.object.name
|
|
75
|
+
: null;
|
|
76
|
+
|
|
77
|
+
if (objName && tracker.isSignal(objName)) {
|
|
78
|
+
context.report({
|
|
79
|
+
node,
|
|
80
|
+
messageId: 'signalWriteInComputed',
|
|
81
|
+
data: { name: objName },
|
|
82
|
+
});
|
|
83
|
+
} else {
|
|
84
|
+
// Could still be a signal — report generic warning
|
|
85
|
+
context.report({
|
|
86
|
+
node,
|
|
87
|
+
messageId: 'setInComputed',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// signal(newValue) — direct call with argument (write via unified getter/setter)
|
|
93
|
+
if (
|
|
94
|
+
node.callee.type === 'Identifier' &&
|
|
95
|
+
tracker.isSignal(node.callee.name) &&
|
|
96
|
+
node.arguments.length > 0
|
|
97
|
+
) {
|
|
98
|
+
context.report({
|
|
99
|
+
node,
|
|
100
|
+
messageId: 'signalWriteInComputed',
|
|
101
|
+
data: { name: node.callee.name },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
// Track computed callback scope entry/exit
|
|
108
|
+
'CallExpression > ArrowFunctionExpression'(node) {
|
|
109
|
+
const parent = node.parent;
|
|
110
|
+
if (
|
|
111
|
+
parent.type === 'CallExpression' &&
|
|
112
|
+
parent.callee.type === 'Identifier' &&
|
|
113
|
+
(parent.callee.name === 'computed' || parent.callee.name === 'useComputed') &&
|
|
114
|
+
parent.arguments[0] === node
|
|
115
|
+
) {
|
|
116
|
+
computedDepth++;
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
'CallExpression > ArrowFunctionExpression:exit'(node) {
|
|
120
|
+
const parent = node.parent;
|
|
121
|
+
if (
|
|
122
|
+
parent.type === 'CallExpression' &&
|
|
123
|
+
parent.callee.type === 'Identifier' &&
|
|
124
|
+
(parent.callee.name === 'computed' || parent.callee.name === 'useComputed') &&
|
|
125
|
+
parent.arguments[0] === node
|
|
126
|
+
) {
|
|
127
|
+
computedDepth--;
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
'CallExpression > FunctionExpression'(node) {
|
|
131
|
+
const parent = node.parent;
|
|
132
|
+
if (
|
|
133
|
+
parent.type === 'CallExpression' &&
|
|
134
|
+
parent.callee.type === 'Identifier' &&
|
|
135
|
+
(parent.callee.name === 'computed' || parent.callee.name === 'useComputed') &&
|
|
136
|
+
parent.arguments[0] === node
|
|
137
|
+
) {
|
|
138
|
+
computedDepth++;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
'CallExpression > FunctionExpression:exit'(node) {
|
|
142
|
+
const parent = node.parent;
|
|
143
|
+
if (
|
|
144
|
+
parent.type === 'CallExpression' &&
|
|
145
|
+
parent.callee.type === 'Identifier' &&
|
|
146
|
+
(parent.callee.name === 'computed' || parent.callee.name === 'useComputed') &&
|
|
147
|
+
parent.arguments[0] === node
|
|
148
|
+
) {
|
|
149
|
+
computedDepth--;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
};
|
|
@@ -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,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: what/signal-call-in-jsx
|
|
3
|
+
*
|
|
4
|
+
* Warn when a signal is used in JSX without calling it.
|
|
5
|
+
* Signals are functions — using them without () in JSX renders "[Function]"
|
|
6
|
+
* instead of the signal's value.
|
|
7
|
+
*
|
|
8
|
+
* This rule specifically targets JSX expression containers, complementing
|
|
9
|
+
* the broader no-uncalled-signals rule with JSX-specific messaging.
|
|
10
|
+
*
|
|
11
|
+
* Bad: <span>{count}</span> — renders "[Function]"
|
|
12
|
+
* Bad: <p>{isLoading && <Spinner />}</p> — always truthy
|
|
13
|
+
*
|
|
14
|
+
* Good: <span>{count()}</span>
|
|
15
|
+
* Good: <p>{isLoading() && <Spinner />}</p>
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createSignalTracker, SIGNAL_METHODS } from '../utils/signal-tracking.js';
|
|
19
|
+
|
|
20
|
+
export default {
|
|
21
|
+
meta: {
|
|
22
|
+
type: 'problem',
|
|
23
|
+
docs: {
|
|
24
|
+
description: 'Require calling signals in JSX expressions — catch missing ()',
|
|
25
|
+
recommended: true,
|
|
26
|
+
},
|
|
27
|
+
messages: {
|
|
28
|
+
signalNotCalledInJsx:
|
|
29
|
+
'"{{name}}" is a signal used in JSX without calling it. ' +
|
|
30
|
+
'Use {{{name}}()} to read the value, or the JSX will render "[Function]".',
|
|
31
|
+
signalNotCalledInJsxLogical:
|
|
32
|
+
'"{{name}}" is a signal used in a JSX conditional without calling it. ' +
|
|
33
|
+
'Signals are always truthy — use {{{name}}() && ...} instead.',
|
|
34
|
+
},
|
|
35
|
+
schema: [],
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
create(context) {
|
|
39
|
+
const tracker = createSignalTracker();
|
|
40
|
+
|
|
41
|
+
function isInsideJSXExpression(node) {
|
|
42
|
+
let current = node.parent;
|
|
43
|
+
while (current) {
|
|
44
|
+
if (current.type === 'JSXExpressionContainer') return true;
|
|
45
|
+
// Stop at function boundaries
|
|
46
|
+
if (
|
|
47
|
+
current.type === 'ArrowFunctionExpression' ||
|
|
48
|
+
current.type === 'FunctionExpression' ||
|
|
49
|
+
current.type === 'FunctionDeclaration'
|
|
50
|
+
) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
current = current.parent;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isBeingCalled(node) {
|
|
59
|
+
return node.parent?.type === 'CallExpression' && node.parent.callee === node;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isMethodAccess(node) {
|
|
63
|
+
return (
|
|
64
|
+
node.parent?.type === 'MemberExpression' &&
|
|
65
|
+
node.parent.object === node &&
|
|
66
|
+
node.parent.property?.type === 'Identifier' &&
|
|
67
|
+
SIGNAL_METHODS.has(node.parent.property.name)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isJSXAttributeValue(node) {
|
|
72
|
+
const exprContainer = node.parent;
|
|
73
|
+
if (exprContainer?.type === 'JSXExpressionContainer') {
|
|
74
|
+
return exprContainer.parent?.type === 'JSXAttribute';
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
VariableDeclarator(node) {
|
|
81
|
+
tracker.visitors.VariableDeclarator(node);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
Identifier(node) {
|
|
85
|
+
const parent = node.parent;
|
|
86
|
+
if (!parent) return;
|
|
87
|
+
|
|
88
|
+
// Skip declaration positions
|
|
89
|
+
if (parent.type === 'VariableDeclarator' && parent.id === node) return;
|
|
90
|
+
if (parent.type === 'FunctionDeclaration' && parent.id === node) return;
|
|
91
|
+
if (parent.type === 'Property' && parent.key === node && !parent.computed) return;
|
|
92
|
+
if (parent.type === 'ImportSpecifier') return;
|
|
93
|
+
if (parent.type === 'ImportDefaultSpecifier') return;
|
|
94
|
+
if (parent.type === 'MemberExpression' && parent.property === node && !parent.computed) return;
|
|
95
|
+
|
|
96
|
+
// Skip if being called — correct usage
|
|
97
|
+
if (isBeingCalled(node)) return;
|
|
98
|
+
// Skip method access
|
|
99
|
+
if (isMethodAccess(node)) return;
|
|
100
|
+
// Skip JSX attribute values (event handlers)
|
|
101
|
+
if (isJSXAttributeValue(node)) return;
|
|
102
|
+
|
|
103
|
+
// Only check signals inside JSX expressions
|
|
104
|
+
if (!tracker.isSignalLike(node.name)) return;
|
|
105
|
+
if (!isInsideJSXExpression(node)) return;
|
|
106
|
+
|
|
107
|
+
// Check if inside a logical expression (&&, ||) — likely a conditional render
|
|
108
|
+
if (
|
|
109
|
+
parent.type === 'LogicalExpression' &&
|
|
110
|
+
parent.left === node
|
|
111
|
+
) {
|
|
112
|
+
context.report({
|
|
113
|
+
node,
|
|
114
|
+
messageId: 'signalNotCalledInJsxLogical',
|
|
115
|
+
data: { name: node.name },
|
|
116
|
+
});
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
context.report({
|
|
121
|
+
node,
|
|
122
|
+
messageId: 'signalNotCalledInJsx',
|
|
123
|
+
data: { name: node.name },
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
};
|
|
@@ -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
|
+
}
|