eslint-plugin-what 0.5.6 → 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 +1 -1
- package/package.json +3 -3
- package/src/index.js +16 -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/signal-call-in-jsx.js +128 -0
package/README.md
CHANGED
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
|
@@ -15,11 +15,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
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';
|
|
18
21
|
|
|
19
22
|
const plugin = {
|
|
20
23
|
meta: {
|
|
21
24
|
name: 'eslint-plugin-what',
|
|
22
|
-
version: '0.
|
|
25
|
+
version: '0.6.0',
|
|
23
26
|
},
|
|
24
27
|
|
|
25
28
|
rules: {
|
|
@@ -29,6 +32,9 @@ const plugin = {
|
|
|
29
32
|
'no-camelcase-events': noCamelcaseEvents,
|
|
30
33
|
'prefer-set': preferSet,
|
|
31
34
|
'no-uncalled-signals': noUncalledSignals,
|
|
35
|
+
'no-h-in-user-code': noHInUserCode,
|
|
36
|
+
'signal-call-in-jsx': signalCallInJsx,
|
|
37
|
+
'no-set-in-computed': noSetInComputed,
|
|
32
38
|
},
|
|
33
39
|
|
|
34
40
|
configs: {},
|
|
@@ -45,6 +51,9 @@ plugin.configs.recommended = {
|
|
|
45
51
|
'what/no-camelcase-events': 'warn',
|
|
46
52
|
'what/no-uncalled-signals': 'warn',
|
|
47
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',
|
|
48
57
|
},
|
|
49
58
|
};
|
|
50
59
|
|
|
@@ -58,6 +67,9 @@ plugin.configs.strict = {
|
|
|
58
67
|
'what/no-camelcase-events': 'error',
|
|
59
68
|
'what/no-uncalled-signals': 'error',
|
|
60
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',
|
|
61
73
|
},
|
|
62
74
|
};
|
|
63
75
|
|
|
@@ -71,6 +83,9 @@ plugin.configs.compiler = {
|
|
|
71
83
|
'what/no-camelcase-events': 'off', // compiler normalizes events
|
|
72
84
|
'what/no-uncalled-signals': 'warn',
|
|
73
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',
|
|
74
89
|
},
|
|
75
90
|
};
|
|
76
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
|
+
};
|
|
@@ -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
|
+
};
|