eslint-plugin-what 0.5.6 → 0.7.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 CHANGED
@@ -161,7 +161,7 @@ count.set(5);
161
161
  ## Links
162
162
 
163
163
  - [Documentation](https://whatfw.com)
164
- - [GitHub](https://github.com/CelsianJs/whatfw)
164
+ - [GitHub](https://github.com/CelsianJs/what-framework)
165
165
 
166
166
  ## License
167
167
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-what",
3
- "version": "0.5.6",
3
+ "version": "0.7.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/whatfw"
28
+ "url": "https://github.com/CelsianJs/what-framework"
29
29
  },
30
30
  "bugs": {
31
- "url": "https://github.com/CelsianJs/whatfw/issues"
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.5.6',
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
+ };