eslint-plugin-what 0.13.9 → 0.13.11

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
@@ -31,6 +31,25 @@ export default [
31
31
 
32
32
  ## Rules
33
33
 
34
+ ### `what/no-destructured-props`
35
+
36
+ Warns when native What components destructure their props during run-once
37
+ setup. Keep the props object and read properties inside reactive bindings:
38
+
39
+ ```jsx
40
+ // Bad: the label is sampled once during component setup.
41
+ function Button({ label }) { return <button>{label}</button>; }
42
+
43
+ // Good: the binding reads the current property when it updates.
44
+ function Button(props) { return <button>{() => props.label}</button>; }
45
+ ```
46
+
47
+ The rule is enabled in recommended/compiler presets and is an error in strict.
48
+ It does not apply this native contract to React compatibility components or
49
+ unrelated domain/event destructuring. It does not promise the compiler will
50
+ automatically wrap arbitrary property reads; explicit accessors work in both
51
+ compiled and automatic-runtime authoring.
52
+
34
53
  ### `what/no-uncalled-signals`
35
54
 
36
55
  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.
@@ -129,6 +148,10 @@ count.set(5);
129
148
  'what/no-camelcase-events': 'warn',
130
149
  'what/no-uncalled-signals': 'warn',
131
150
  'what/prefer-set': 'off',
151
+ 'what/no-h-in-user-code': 'warn',
152
+ 'what/signal-call-in-jsx': 'warn',
153
+ 'what/no-set-in-computed': 'error',
154
+ 'what/no-destructured-props': 'warn',
132
155
  }
133
156
  ```
134
157
 
@@ -142,6 +165,10 @@ count.set(5);
142
165
  'what/no-camelcase-events': 'error',
143
166
  'what/no-uncalled-signals': 'error',
144
167
  'what/prefer-set': 'warn',
168
+ 'what/no-h-in-user-code': 'error',
169
+ 'what/signal-call-in-jsx': 'error',
170
+ 'what/no-set-in-computed': 'error',
171
+ 'what/no-destructured-props': 'error',
145
172
  }
146
173
  ```
147
174
 
@@ -155,6 +182,10 @@ count.set(5);
155
182
  'what/no-camelcase-events': 'off', // compiler normalizes events
156
183
  'what/no-uncalled-signals': 'warn',
157
184
  'what/prefer-set': 'off',
185
+ 'what/no-h-in-user-code': 'warn',
186
+ 'what/signal-call-in-jsx': 'off',
187
+ 'what/no-set-in-computed': 'error',
188
+ 'what/no-destructured-props': ['warn', { assumeNative: true }],
158
189
  }
159
190
  ```
160
191
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-what",
3
- "version": "0.13.9",
3
+ "version": "0.13.11",
4
4
  "description": "ESLint rules for What Framework — catch signal bugs, enforce patterns",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -20,6 +20,7 @@ import noUncalledSignals from './rules/no-uncalled-signals.js';
20
20
  import noHInUserCode from './rules/no-h-in-user-code.js';
21
21
  import signalCallInJsx from './rules/signal-call-in-jsx.js';
22
22
  import noSetInComputed from './rules/no-set-in-computed.js';
23
+ import noDestructuredProps from './rules/no-destructured-props.js';
23
24
 
24
25
  // Read the real version from package.json so plugin meta never goes stale.
25
26
  const require = createRequire(import.meta.url);
@@ -41,6 +42,7 @@ const plugin = {
41
42
  'no-h-in-user-code': noHInUserCode,
42
43
  'signal-call-in-jsx': signalCallInJsx,
43
44
  'no-set-in-computed': noSetInComputed,
45
+ 'no-destructured-props': noDestructuredProps,
44
46
  },
45
47
 
46
48
  configs: {},
@@ -94,6 +96,7 @@ plugin.configs.recommended = {
94
96
  'what/no-h-in-user-code': 'warn',
95
97
  'what/signal-call-in-jsx': 'warn',
96
98
  'what/no-set-in-computed': 'error',
99
+ 'what/no-destructured-props': 'warn',
97
100
  },
98
101
  };
99
102
 
@@ -113,6 +116,7 @@ plugin.configs.strict = {
113
116
  'what/no-h-in-user-code': 'error',
114
117
  'what/signal-call-in-jsx': 'error',
115
118
  'what/no-set-in-computed': 'error',
119
+ 'what/no-destructured-props': 'error',
116
120
  },
117
121
  };
118
122
 
@@ -132,6 +136,7 @@ plugin.configs.compiler = {
132
136
  'what/no-h-in-user-code': 'warn',
133
137
  'what/signal-call-in-jsx': 'off', // compiler handles signal wrapping in JSX
134
138
  'what/no-set-in-computed': 'error',
139
+ 'what/no-destructured-props': ['warn', { assumeNative: true }],
135
140
  },
136
141
  };
137
142
 
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Native What components run once. Destructuring their props during setup
3
+ * snapshots values before the compiler's JSX effects can track property reads.
4
+ * React compatibility components rerender and must not inherit this restriction.
5
+ */
6
+ export default {
7
+ meta: {
8
+ type: 'problem',
9
+ docs: { description: 'Keep native component props reactive instead of destructuring at setup', recommended: true },
10
+ schema: [{ type: 'object', properties: { assumeNative: { type: 'boolean' } }, additionalProperties: false }],
11
+ messages: {
12
+ snapshot: 'Destructuring native component props captures their initial values. Read props.property inside a reactive callback instead.',
13
+ },
14
+ },
15
+
16
+ create(context) {
17
+ const sourceCode = context.sourceCode;
18
+ let native = context.options[0]?.assumeNative === true;
19
+ let react = /(?:^|[/\\])react-compat(?:[/\\]|$)/.test(context.filename);
20
+ const hNames = new Set();
21
+ const functions = [];
22
+
23
+ function enter(node) {
24
+ const name = node.id?.name || (node.parent?.type === 'VariableDeclarator' ? node.parent.id.name : '');
25
+ const parameter = node.params[0]?.type === 'AssignmentPattern' ? node.params[0].left : node.params[0];
26
+ const component = /^[A-Z]/.test(name || '') || !name && node.parent?.type === 'ExportDefaultDeclaration';
27
+ functions.push({ node, parameter, component, returnedTree: false, snapshots: [] });
28
+ }
29
+
30
+ function exit() {
31
+ const fn = functions.pop();
32
+ if (!native || react || !fn.component || !fn.returnedTree) return;
33
+ if (fn.parameter?.type === 'ObjectPattern' && fn.parameter.properties.length > 0) {
34
+ context.report({ node: fn.parameter, messageId: 'snapshot' });
35
+ }
36
+ for (const node of fn.snapshots) context.report({ node, messageId: 'snapshot' });
37
+ }
38
+
39
+ // Resolve the binding, not just its spelling: a block-scoped domain object
40
+ // or a callback parameter named `props` is not the component's parameter.
41
+ function isPropsRead(node, parameter) {
42
+ if (node.type !== 'Identifier' || parameter?.type !== 'Identifier' || node.name !== parameter.name) return false;
43
+ let scope = sourceCode.getScope(node);
44
+ while (scope) {
45
+ const variable = scope.set.get(node.name);
46
+ if (variable) return variable.defs.some(def => def.type === 'Parameter' && def.name === parameter);
47
+ scope = scope.upper;
48
+ }
49
+ return false;
50
+ }
51
+
52
+ function returnedTree(node) {
53
+ const fn = functions[functions.length - 1];
54
+ if (!fn?.component) return;
55
+ let parent = node;
56
+ while (parent && parent !== fn.node) {
57
+ if (parent.type === 'ReturnStatement' || parent === fn.node.body && fn.node.type === 'ArrowFunctionExpression') {
58
+ fn.returnedTree = true;
59
+ return;
60
+ }
61
+ parent = parent.parent;
62
+ }
63
+ }
64
+
65
+ return {
66
+ Program(node) {
67
+ // Imports may legally follow declarations. Determine the runtime
68
+ // before visiting any component, including React compatibility files.
69
+ for (const entry of node.body) {
70
+ if (entry.type !== 'ImportDeclaration') continue;
71
+ const from = entry.source.value;
72
+ if (/^(?:react|what-react)(?:\/|$)/.test(from)) react = true;
73
+ if (/^(?:what-framework|what-core)(?:\/|$)/.test(from)) {
74
+ native = true;
75
+ for (const spec of entry.specifiers) {
76
+ if (spec.type === 'ImportSpecifier' && spec.imported.name === 'h') hNames.add(spec.local.name);
77
+ }
78
+ }
79
+ }
80
+ },
81
+ FunctionDeclaration: enter,
82
+ FunctionExpression: enter,
83
+ ArrowFunctionExpression: enter,
84
+ 'FunctionDeclaration:exit': exit,
85
+ 'FunctionExpression:exit': exit,
86
+ 'ArrowFunctionExpression:exit': exit,
87
+ JSXElement: returnedTree,
88
+ JSXFragment: returnedTree,
89
+ CallExpression(node) {
90
+ if (node.callee.type === 'Identifier' && hNames.has(node.callee.name)) returnedTree(node);
91
+ },
92
+ VariableDeclarator(node) {
93
+ const fn = functions[functions.length - 1];
94
+ if (fn?.component && node.id.type === 'ObjectPattern' && node.id.properties.length > 0 && node.init && isPropsRead(node.init, fn.parameter)) {
95
+ fn.snapshots.push(node.id);
96
+ }
97
+ },
98
+ };
99
+ },
100
+ };