eslint-plugin-what 0.5.3
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/package.json +34 -0
- package/src/index.js +72 -0
- package/src/rules/no-camelcase-events.js +104 -0
- package/src/rules/no-signal-in-effect-deps.js +75 -0
- package/src/rules/no-signal-write-in-render.js +126 -0
- package/src/rules/prefer-set.js +73 -0
- package/src/rules/reactive-jsx-children.js +91 -0
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eslint-plugin-what",
|
|
3
|
+
"version": "0.5.3",
|
|
4
|
+
"description": "ESLint rules for What Framework — catch signal bugs, enforce patterns",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"eslint",
|
|
15
|
+
"eslintplugin",
|
|
16
|
+
"what",
|
|
17
|
+
"framework",
|
|
18
|
+
"signals",
|
|
19
|
+
"reactive"
|
|
20
|
+
],
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"eslint": ">=9.0.0"
|
|
23
|
+
},
|
|
24
|
+
"author": "",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/zvndev/what-fw"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/zvndev/what-fw/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://whatframework.dev"
|
|
34
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* eslint-plugin-what
|
|
3
|
+
*
|
|
4
|
+
* ESLint rules for What Framework — catch signal bugs, enforce patterns.
|
|
5
|
+
* Designed for ESLint 9+ flat config.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import what from 'eslint-plugin-what';
|
|
9
|
+
* export default [what.configs.recommended];
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import noSignalInEffectDeps from './rules/no-signal-in-effect-deps.js';
|
|
13
|
+
import reactiveJsxChildren from './rules/reactive-jsx-children.js';
|
|
14
|
+
import noSignalWriteInRender from './rules/no-signal-write-in-render.js';
|
|
15
|
+
import noCamelcaseEvents from './rules/no-camelcase-events.js';
|
|
16
|
+
import preferSet from './rules/prefer-set.js';
|
|
17
|
+
|
|
18
|
+
const plugin = {
|
|
19
|
+
meta: {
|
|
20
|
+
name: 'eslint-plugin-what',
|
|
21
|
+
version: '0.5.2',
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
rules: {
|
|
25
|
+
'no-signal-in-effect-deps': noSignalInEffectDeps,
|
|
26
|
+
'reactive-jsx-children': reactiveJsxChildren,
|
|
27
|
+
'no-signal-write-in-render': noSignalWriteInRender,
|
|
28
|
+
'no-camelcase-events': noCamelcaseEvents,
|
|
29
|
+
'prefer-set': preferSet,
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
configs: {},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Flat config presets (ESLint 9+)
|
|
36
|
+
|
|
37
|
+
plugin.configs.recommended = {
|
|
38
|
+
plugins: { what: plugin },
|
|
39
|
+
rules: {
|
|
40
|
+
'what/no-signal-in-effect-deps': 'warn',
|
|
41
|
+
'what/reactive-jsx-children': 'warn',
|
|
42
|
+
'what/no-signal-write-in-render': 'warn',
|
|
43
|
+
'what/no-camelcase-events': 'warn',
|
|
44
|
+
'what/prefer-set': 'off',
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Stricter config — all rules as errors + prefer-set
|
|
49
|
+
plugin.configs.strict = {
|
|
50
|
+
plugins: { what: plugin },
|
|
51
|
+
rules: {
|
|
52
|
+
'what/no-signal-in-effect-deps': 'error',
|
|
53
|
+
'what/reactive-jsx-children': 'error',
|
|
54
|
+
'what/no-signal-write-in-render': 'error',
|
|
55
|
+
'what/no-camelcase-events': 'error',
|
|
56
|
+
'what/prefer-set': 'warn',
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Config for projects using the What compiler (disables rules the compiler handles)
|
|
61
|
+
plugin.configs.compiler = {
|
|
62
|
+
plugins: { what: plugin },
|
|
63
|
+
rules: {
|
|
64
|
+
'what/no-signal-in-effect-deps': 'warn',
|
|
65
|
+
'what/reactive-jsx-children': 'off', // compiler handles reactive wrapping
|
|
66
|
+
'what/no-signal-write-in-render': 'warn',
|
|
67
|
+
'what/no-camelcase-events': 'off', // compiler normalizes events
|
|
68
|
+
'what/prefer-set': 'off',
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default plugin;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: what/no-camelcase-events
|
|
3
|
+
*
|
|
4
|
+
* Warn on camelCase event handlers (onClick, onChange, etc.) in JSX when not
|
|
5
|
+
* using the What compiler. Without the compiler, esbuild passes props through
|
|
6
|
+
* directly to the DOM, and DOM event handlers must be lowercase (onclick, onchange).
|
|
7
|
+
*
|
|
8
|
+
* The compiler normalizes these automatically, so this rule only applies to
|
|
9
|
+
* projects using esbuild's built-in JSX transform instead of what-compiler.
|
|
10
|
+
*
|
|
11
|
+
* Bad (without compiler): <button onClick={fn} />
|
|
12
|
+
* Good (without compiler): <button onclick={fn} />
|
|
13
|
+
* Good (with compiler): <button onClick={fn} /> — compiler handles it
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Common DOM events that have camelCase variants
|
|
17
|
+
const CAMEL_EVENTS = new Map([
|
|
18
|
+
['onClick', 'onclick'],
|
|
19
|
+
['onChange', 'onchange'],
|
|
20
|
+
['onInput', 'oninput'],
|
|
21
|
+
['onSubmit', 'onsubmit'],
|
|
22
|
+
['onFocus', 'onfocus'],
|
|
23
|
+
['onBlur', 'onblur'],
|
|
24
|
+
['onKeyDown', 'onkeydown'],
|
|
25
|
+
['onKeyUp', 'onkeyup'],
|
|
26
|
+
['onKeyPress', 'onkeypress'],
|
|
27
|
+
['onMouseDown', 'onmousedown'],
|
|
28
|
+
['onMouseUp', 'onmouseup'],
|
|
29
|
+
['onMouseMove', 'onmousemove'],
|
|
30
|
+
['onMouseEnter', 'onmouseenter'],
|
|
31
|
+
['onMouseLeave', 'onmouseleave'],
|
|
32
|
+
['onMouseOver', 'onmouseover'],
|
|
33
|
+
['onMouseOut', 'onmouseout'],
|
|
34
|
+
['onTouchStart', 'ontouchstart'],
|
|
35
|
+
['onTouchEnd', 'ontouchend'],
|
|
36
|
+
['onTouchMove', 'ontouchmove'],
|
|
37
|
+
['onScroll', 'onscroll'],
|
|
38
|
+
['onWheel', 'onwheel'],
|
|
39
|
+
['onDragStart', 'ondragstart'],
|
|
40
|
+
['onDragEnd', 'ondragend'],
|
|
41
|
+
['onDragOver', 'ondragover'],
|
|
42
|
+
['onDrop', 'ondrop'],
|
|
43
|
+
['onContextMenu', 'oncontextmenu'],
|
|
44
|
+
['onDoubleClick', 'ondblclick'],
|
|
45
|
+
['onPointerDown', 'onpointerdown'],
|
|
46
|
+
['onPointerUp', 'onpointerup'],
|
|
47
|
+
['onPointerMove', 'onpointermove'],
|
|
48
|
+
['onAnimationEnd', 'onanimationend'],
|
|
49
|
+
['onTransitionEnd', 'ontransitionend'],
|
|
50
|
+
['onLoad', 'onload'],
|
|
51
|
+
['onError', 'onerror'],
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
export default {
|
|
55
|
+
meta: {
|
|
56
|
+
type: 'problem',
|
|
57
|
+
docs: {
|
|
58
|
+
description: 'Disallow camelCase event handlers in JSX without the compiler',
|
|
59
|
+
recommended: true,
|
|
60
|
+
},
|
|
61
|
+
fixable: 'code',
|
|
62
|
+
messages: {
|
|
63
|
+
camelCaseEvent:
|
|
64
|
+
'"{{name}}" won\'t work without the What compiler. Use "{{fix}}" instead.',
|
|
65
|
+
},
|
|
66
|
+
schema: [
|
|
67
|
+
{
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
hasCompiler: {
|
|
71
|
+
type: 'boolean',
|
|
72
|
+
description: 'Set to true if the project uses what-compiler (skips this rule)',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
additionalProperties: false,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
create(context) {
|
|
81
|
+
const options = context.options[0] || {};
|
|
82
|
+
if (options.hasCompiler === true) return {};
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
JSXAttribute(node) {
|
|
86
|
+
if (node.name.type !== 'JSXIdentifier') return;
|
|
87
|
+
|
|
88
|
+
const name = node.name.name;
|
|
89
|
+
const fix = CAMEL_EVENTS.get(name);
|
|
90
|
+
|
|
91
|
+
if (fix) {
|
|
92
|
+
context.report({
|
|
93
|
+
node: node.name,
|
|
94
|
+
messageId: 'camelCaseEvent',
|
|
95
|
+
data: { name, fix },
|
|
96
|
+
fix(fixer) {
|
|
97
|
+
return fixer.replaceText(node.name, fix);
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: what/no-signal-in-effect-deps
|
|
3
|
+
*
|
|
4
|
+
* Warn when passing signal getter functions to useEffect dependency arrays.
|
|
5
|
+
* Signal getters create new function references, so deps always appear "changed",
|
|
6
|
+
* causing the effect to re-run on every render cycle.
|
|
7
|
+
*
|
|
8
|
+
* Bad: useEffect(() => { ... }, [count]) // count is a signal getter fn
|
|
9
|
+
* Good: useEffect(() => { ... }, [count()]) // use the value instead
|
|
10
|
+
* Good: useEffect(() => { count(); }, []) // or rely on auto-tracking
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'problem',
|
|
16
|
+
docs: {
|
|
17
|
+
description: 'Disallow signal getters in useEffect dependency arrays',
|
|
18
|
+
recommended: true,
|
|
19
|
+
},
|
|
20
|
+
messages: {
|
|
21
|
+
signalInDeps:
|
|
22
|
+
'Signal getter "{{name}}" in useEffect deps will cause infinite re-runs. ' +
|
|
23
|
+
'Use {{name}}() for the value, or remove deps to rely on auto-tracking.',
|
|
24
|
+
},
|
|
25
|
+
schema: [],
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
create(context) {
|
|
29
|
+
// Track variables initialized from signal/useSignal/computed calls
|
|
30
|
+
const signalVars = new Set();
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
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
|
+
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
CallExpression(node) {
|
|
48
|
+
// Match useEffect(fn, [deps])
|
|
49
|
+
if (
|
|
50
|
+
node.callee.type !== 'Identifier' ||
|
|
51
|
+
node.callee.name !== 'useEffect'
|
|
52
|
+
) return;
|
|
53
|
+
|
|
54
|
+
const depsArg = node.arguments[1];
|
|
55
|
+
if (!depsArg || depsArg.type !== 'ArrayExpression') return;
|
|
56
|
+
|
|
57
|
+
for (const element of depsArg.elements) {
|
|
58
|
+
if (!element) continue;
|
|
59
|
+
|
|
60
|
+
// Direct signal reference: useEffect(fn, [count])
|
|
61
|
+
if (
|
|
62
|
+
element.type === 'Identifier' &&
|
|
63
|
+
signalVars.has(element.name)
|
|
64
|
+
) {
|
|
65
|
+
context.report({
|
|
66
|
+
node: element,
|
|
67
|
+
messageId: 'signalInDeps',
|
|
68
|
+
data: { name: element.name },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: what/no-signal-write-in-render
|
|
3
|
+
*
|
|
4
|
+
* Warn when writing to a signal outside of event handlers, effects, or callbacks.
|
|
5
|
+
* Signal writes during render (the component function body) can cause infinite
|
|
6
|
+
* re-render loops because the write triggers effects that re-run the component.
|
|
7
|
+
*
|
|
8
|
+
* Bad: function App() { count(count() + 1); return ... }
|
|
9
|
+
* Good: function App() { return <button onclick={() => count(c => c + 1)} /> }
|
|
10
|
+
* Good: useEffect(() => { count(0); })
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'problem',
|
|
16
|
+
docs: {
|
|
17
|
+
description: 'Disallow signal writes in the render phase (component function body)',
|
|
18
|
+
recommended: true,
|
|
19
|
+
},
|
|
20
|
+
messages: {
|
|
21
|
+
signalWriteInRender:
|
|
22
|
+
'Signal write to "{{name}}" during render may cause infinite loops. ' +
|
|
23
|
+
'Move signal writes into event handlers, effects, or callbacks.',
|
|
24
|
+
},
|
|
25
|
+
schema: [],
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
create(context) {
|
|
29
|
+
const signalVars = new Set();
|
|
30
|
+
|
|
31
|
+
// Track whether we're inside a "safe" write context
|
|
32
|
+
function isInsideSafeContext(node) {
|
|
33
|
+
let current = node.parent;
|
|
34
|
+
while (current) {
|
|
35
|
+
// Inside event handler (arrow fn or function expression assigned to on* prop or as callback arg)
|
|
36
|
+
if (
|
|
37
|
+
current.type === 'ArrowFunctionExpression' ||
|
|
38
|
+
current.type === 'FunctionExpression'
|
|
39
|
+
) {
|
|
40
|
+
// Check if it's an event handler prop: onclick={() => ...}
|
|
41
|
+
if (
|
|
42
|
+
current.parent?.type === 'JSXExpressionContainer' &&
|
|
43
|
+
current.parent?.parent?.type === 'JSXAttribute'
|
|
44
|
+
) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
// Check if it's an event handler prop (object property): { onclick: () => ... }
|
|
48
|
+
if (
|
|
49
|
+
current.parent?.type === 'Property' &&
|
|
50
|
+
current.parent?.key?.type === 'Identifier' &&
|
|
51
|
+
/^on[a-z]/.test(current.parent.key.name)
|
|
52
|
+
) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
// Callback passed to useEffect, effect, setTimeout, etc.
|
|
56
|
+
if (current.parent?.type === 'CallExpression') {
|
|
57
|
+
const callee = current.parent.callee;
|
|
58
|
+
if (
|
|
59
|
+
callee.type === 'Identifier' &&
|
|
60
|
+
['useEffect', 'effect', 'setTimeout', 'setInterval', 'requestAnimationFrame',
|
|
61
|
+
'queueMicrotask', 'batch', 'onMount', 'onCleanup', 'addEventListener'].includes(callee.name)
|
|
62
|
+
) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Generic arrow/function inside another arrow/function — assume nested callback is safe
|
|
67
|
+
if (
|
|
68
|
+
current.parent?.type === 'ArrowFunctionExpression' ||
|
|
69
|
+
current.parent?.type === 'FunctionExpression'
|
|
70
|
+
) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
current = current.parent;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
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
|
+
}
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
CallExpression(node) {
|
|
93
|
+
// Check for signal writes: count(value), count.set(value)
|
|
94
|
+
let signalName = null;
|
|
95
|
+
|
|
96
|
+
// Direct call: count(value) — with at least one argument (0-arg is a read)
|
|
97
|
+
if (
|
|
98
|
+
node.callee.type === 'Identifier' &&
|
|
99
|
+
signalVars.has(node.callee.name) &&
|
|
100
|
+
node.arguments.length > 0
|
|
101
|
+
) {
|
|
102
|
+
signalName = node.callee.name;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Method call: count.set(value)
|
|
106
|
+
if (
|
|
107
|
+
node.callee.type === 'MemberExpression' &&
|
|
108
|
+
node.callee.object.type === 'Identifier' &&
|
|
109
|
+
signalVars.has(node.callee.object.name) &&
|
|
110
|
+
node.callee.property.type === 'Identifier' &&
|
|
111
|
+
node.callee.property.name === 'set'
|
|
112
|
+
) {
|
|
113
|
+
signalName = node.callee.object.name;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (signalName && !isInsideSafeContext(node)) {
|
|
117
|
+
context.report({
|
|
118
|
+
node,
|
|
119
|
+
messageId: 'signalWriteInRender',
|
|
120
|
+
data: { name: signalName },
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: what/prefer-set
|
|
3
|
+
*
|
|
4
|
+
* Suggest using sig.set(value) instead of sig(value) for signal writes.
|
|
5
|
+
* The unified getter/setter pattern sig(value) is valid but ambiguous —
|
|
6
|
+
* sig.set(value) makes the write intent explicit and easier to grep/review.
|
|
7
|
+
*
|
|
8
|
+
* Bad: count(5) // is this a read or write?
|
|
9
|
+
* Bad: count(c => c + 1) // updater pattern
|
|
10
|
+
* Good: count.set(5)
|
|
11
|
+
* Good: count.set(c => c + 1)
|
|
12
|
+
*
|
|
13
|
+
* This rule is off by default (style preference, not a bug).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
meta: {
|
|
18
|
+
type: 'suggestion',
|
|
19
|
+
docs: {
|
|
20
|
+
description: 'Prefer sig.set(value) over sig(value) for signal writes',
|
|
21
|
+
recommended: false,
|
|
22
|
+
},
|
|
23
|
+
fixable: 'code',
|
|
24
|
+
messages: {
|
|
25
|
+
preferSet:
|
|
26
|
+
'Prefer "{{name}}.set({{arg}})" over "{{name}}({{arg}})" for explicit signal writes.',
|
|
27
|
+
},
|
|
28
|
+
schema: [],
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
create(context) {
|
|
32
|
+
const signalVars = new Set();
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
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
|
+
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
CallExpression(node) {
|
|
48
|
+
// Only match: signalVar(value) with exactly 1 argument
|
|
49
|
+
if (
|
|
50
|
+
node.callee.type !== 'Identifier' ||
|
|
51
|
+
!signalVars.has(node.callee.name) ||
|
|
52
|
+
node.arguments.length !== 1
|
|
53
|
+
) return;
|
|
54
|
+
|
|
55
|
+
// Already using .set() — skip
|
|
56
|
+
if (node.callee.type === 'MemberExpression') return;
|
|
57
|
+
|
|
58
|
+
const name = node.callee.name;
|
|
59
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
60
|
+
const argText = sourceCode.getText(node.arguments[0]);
|
|
61
|
+
|
|
62
|
+
context.report({
|
|
63
|
+
node,
|
|
64
|
+
messageId: 'preferSet',
|
|
65
|
+
data: { name, arg: argText },
|
|
66
|
+
fix(fixer) {
|
|
67
|
+
return fixer.replaceText(node, `${name}.set(${argText})`);
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: what/reactive-jsx-children
|
|
3
|
+
*
|
|
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
|
|
6
|
+
* signal read like {count()} won't be reactive — it captures the value once.
|
|
7
|
+
*
|
|
8
|
+
* The rule checks if the project uses what-compiler (via config option or
|
|
9
|
+
* vite.config presence). If no compiler is detected, it warns on bare signal
|
|
10
|
+
* reads in JSX expression containers.
|
|
11
|
+
*
|
|
12
|
+
* Bad (without compiler): <p>{count()}</p>
|
|
13
|
+
* Good (without compiler): <p>{() => count()}</p>
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
meta: {
|
|
18
|
+
type: 'problem',
|
|
19
|
+
docs: {
|
|
20
|
+
description: 'Require reactive function wrappers for signal reads in JSX when not using the compiler',
|
|
21
|
+
recommended: true,
|
|
22
|
+
},
|
|
23
|
+
messages: {
|
|
24
|
+
bareSignalRead:
|
|
25
|
+
'Signal read "{{name}}()" in JSX won\'t be reactive without the What compiler. ' +
|
|
26
|
+
'Wrap in a function: {() => {{name}}()}',
|
|
27
|
+
},
|
|
28
|
+
schema: [
|
|
29
|
+
{
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
hasCompiler: {
|
|
33
|
+
type: 'boolean',
|
|
34
|
+
description: 'Set to true if the project uses what-compiler (auto-detected if not set)',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
create(context) {
|
|
43
|
+
const options = context.options[0] || {};
|
|
44
|
+
|
|
45
|
+
// If the user explicitly says they have the compiler, skip all checks
|
|
46
|
+
if (options.hasCompiler === true) return {};
|
|
47
|
+
|
|
48
|
+
// Track signal variables
|
|
49
|
+
const signalVars = new Set();
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
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
|
+
}
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
// JSX expression: {count()}
|
|
65
|
+
JSXExpressionContainer(node) {
|
|
66
|
+
const expr = node.expression;
|
|
67
|
+
if (!expr || expr.type === 'JSXEmptyExpression') return;
|
|
68
|
+
|
|
69
|
+
// Skip if parent is an attribute (e.g., className={count()}) — only check children
|
|
70
|
+
if (node.parent.type !== 'JSXElement' && node.parent.type !== 'JSXFragment') return;
|
|
71
|
+
|
|
72
|
+
// Already wrapped in arrow: {() => count()} — OK
|
|
73
|
+
if (expr.type === 'ArrowFunctionExpression' || expr.type === 'FunctionExpression') return;
|
|
74
|
+
|
|
75
|
+
// Direct signal call: {count()}
|
|
76
|
+
if (
|
|
77
|
+
expr.type === 'CallExpression' &&
|
|
78
|
+
expr.callee.type === 'Identifier' &&
|
|
79
|
+
signalVars.has(expr.callee.name) &&
|
|
80
|
+
expr.arguments.length === 0
|
|
81
|
+
) {
|
|
82
|
+
context.report({
|
|
83
|
+
node: expr,
|
|
84
|
+
messageId: 'bareSignalRead',
|
|
85
|
+
data: { name: expr.callee.name },
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
};
|