kensington-eslint-plugin 0.6.0 → 0.6.1
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 +22 -1
- package/index.js +4 -0
- package/package.json +1 -1
- package/rules/prefer-subscribe-in-effect.js +74 -0
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ export default [
|
|
|
56
56
|
What `strict` changes on top of `recommended`:
|
|
57
57
|
|
|
58
58
|
- **Adds `no-helper-function-trap`** (error). The most valuable single rule the plugin ships.
|
|
59
|
-
- **Promotes from `warn` to `error`**: `no-signal-async-write`, `no-ignored-effect-return`, `prefer-value-in-async`, `no-out-of-scope-reactive-reference`. Real reactive-correctness issues; strict mode chooses zero silent misses over tolerance of false positives.
|
|
59
|
+
- **Promotes from `warn` to `error`**: `no-signal-async-write`, `no-ignored-effect-return`, `prefer-value-in-async`, `prefer-subscribe-in-effect`, `no-out-of-scope-reactive-reference`. Real reactive-correctness issues; strict mode chooses zero silent misses over tolerance of false positives.
|
|
60
60
|
|
|
61
61
|
Use `strict` if you want CI to fail on any reactive-correctness issue, or if you're using an agent-driven workflow that benefits from harder enforcement. Use `recommended` for production codebases that prefer the warnings as guidance.
|
|
62
62
|
|
|
@@ -124,6 +124,7 @@ Because this is a standard ESLint plugin, it works anywhere ESLint runs with no
|
|
|
124
124
|
| [`no-signal-async-write`](#no-signal-async-write) | Disallow writing a signal in an async callback when it was read in the enclosing `effect()` | warn | error |
|
|
125
125
|
| [`no-ignored-effect-return`](#no-ignored-effect-return) | Require capturing the return value of `effect()` inside a function | warn | error |
|
|
126
126
|
| [`prefer-value-in-async`](#prefer-value-in-async) | Prefer `.value` over `.get()` inside async callbacks within an `effect()` | warn | error |
|
|
127
|
+
| [`prefer-subscribe-in-effect`](#prefer-subscribe-in-effect) | Prefer `.subscribe()` for trigger-only `.get()` calls inside an `effect()` | warn | error |
|
|
127
128
|
| [`no-new-computed-in-effect`](#no-new-computed-in-effect) | Disallow creating a new `computed()` inside an `effect()` body | error | error |
|
|
128
129
|
| [`no-new-signal-in-computed`](#no-new-signal-in-computed) | Require a stable key for `signal()` calls inside a `computed()` body | error | error |
|
|
129
130
|
| [`no-unsafe-literal`](#no-unsafe-literal) | Disallow `.unsafeLiteral()` calls that bypass XSS protection | error | error |
|
|
@@ -321,6 +322,26 @@ effect(() => {
|
|
|
321
322
|
|
|
322
323
|
---
|
|
323
324
|
|
|
325
|
+
### `prefer-subscribe-in-effect`
|
|
326
|
+
|
|
327
|
+
Use `.subscribe()` when an effect reads a signal only to subscribe to its changes and does not need the current value. The alias makes the trigger-only intent clear.
|
|
328
|
+
|
|
329
|
+
```js
|
|
330
|
+
// Bad
|
|
331
|
+
effect(() => {
|
|
332
|
+
count.get(); // warn. The value is ignored.
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Good
|
|
336
|
+
effect(() => {
|
|
337
|
+
count.subscribe();
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
The rule leaves value reads used in expressions, assignments, and nested callbacks alone.
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
324
345
|
### `no-new-computed-in-effect`
|
|
325
346
|
|
|
326
347
|
Creating `computed()` inside an `effect()` creates a new orphaned derived signal on every run. The previous one silently loses its subscriber with no cleanup.
|
package/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import consistentContentLayout from './rules/consistent-content-layout.js';
|
|
|
25
25
|
import noHelperFunctionTrap from './rules/no-helper-function-trap.js';
|
|
26
26
|
import requireReactiveKey from './rules/require-reactive-key.js';
|
|
27
27
|
import noAsyncSet from './rules/no-async-set.js';
|
|
28
|
+
import preferSubscribeInEffect from './rules/prefer-subscribe-in-effect.js';
|
|
28
29
|
|
|
29
30
|
const plugin = {
|
|
30
31
|
meta: { name: 'eslint-plugin-kensington' },
|
|
@@ -56,6 +57,7 @@ const plugin = {
|
|
|
56
57
|
'no-helper-function-trap': noHelperFunctionTrap,
|
|
57
58
|
'require-reactive-key': requireReactiveKey,
|
|
58
59
|
'no-async-set': noAsyncSet,
|
|
60
|
+
'prefer-subscribe-in-effect': preferSubscribeInEffect,
|
|
59
61
|
},
|
|
60
62
|
configs: {},
|
|
61
63
|
};
|
|
@@ -80,6 +82,7 @@ plugin.configs.recommended = {
|
|
|
80
82
|
'kensington/no-out-of-scope-reactive-reference': 'warn',
|
|
81
83
|
'kensington/no-helper-function-trap': 'warn',
|
|
82
84
|
'kensington/no-async-set': 'error',
|
|
85
|
+
'kensington/prefer-subscribe-in-effect': 'warn',
|
|
83
86
|
},
|
|
84
87
|
};
|
|
85
88
|
|
|
@@ -102,6 +105,7 @@ plugin.configs.strict = {
|
|
|
102
105
|
'kensington/prefer-value-in-async': 'error',
|
|
103
106
|
'kensington/no-out-of-scope-reactive-reference': 'error',
|
|
104
107
|
'kensington/no-helper-function-trap': 'error',
|
|
108
|
+
'kensington/prefer-subscribe-in-effect': 'error',
|
|
105
109
|
},
|
|
106
110
|
};
|
|
107
111
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Reports trigger-only .get() calls in an effect() callback. `.subscribe()` is
|
|
2
|
+
// an alias for `.get()` that makes it clear the value is intentionally ignored.
|
|
3
|
+
export default {
|
|
4
|
+
meta: {
|
|
5
|
+
type: 'suggestion',
|
|
6
|
+
docs: {
|
|
7
|
+
description: 'prefer .subscribe() over trigger-only .get() calls in effect() callbacks',
|
|
8
|
+
},
|
|
9
|
+
messages: {
|
|
10
|
+
preferSubscribeInEffect:
|
|
11
|
+
'Use .subscribe() for a trigger-only read inside an effect. ' +
|
|
12
|
+
'It makes clear that the signal value is intentionally ignored.',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
create(context) {
|
|
17
|
+
const effectNames = new Set();
|
|
18
|
+
// Each frame is { type: 'effect'|'other', callback? }. Nested functions are
|
|
19
|
+
// excluded because their .get() calls may consume a value independently of
|
|
20
|
+
// the surrounding effect's trigger-only reads.
|
|
21
|
+
const fnStack = [];
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
ImportDeclaration(node) {
|
|
25
|
+
if (node.source.value !== 'kensington') { return; }
|
|
26
|
+
for (const spec of node.specifiers) {
|
|
27
|
+
if (spec.type !== 'ImportSpecifier') { continue; }
|
|
28
|
+
if (spec.imported.name === 'effect') { effectNames.add(spec.local.name); }
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
':matches(ArrowFunctionExpression, FunctionExpression)'(node) {
|
|
33
|
+
const { parent } = node;
|
|
34
|
+
if (
|
|
35
|
+
parent.type === 'CallExpression' &&
|
|
36
|
+
parent.arguments[0] === node &&
|
|
37
|
+
parent.callee.type === 'Identifier' &&
|
|
38
|
+
effectNames.has(parent.callee.name)
|
|
39
|
+
) {
|
|
40
|
+
fnStack.push({ type: 'effect', callback: node });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fnStack.push({ type: 'other' });
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
':matches(ArrowFunctionExpression, FunctionExpression):exit'() {
|
|
48
|
+
fnStack.pop();
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
CallExpression(node) {
|
|
52
|
+
const frame = fnStack[fnStack.length - 1];
|
|
53
|
+
if (!frame || frame.type !== 'effect') { return; }
|
|
54
|
+
if (
|
|
55
|
+
node.callee.type !== 'MemberExpression' ||
|
|
56
|
+
node.callee.object.type !== 'Identifier' ||
|
|
57
|
+
node.callee.property.type !== 'Identifier' ||
|
|
58
|
+
node.callee.property.name !== 'get' ||
|
|
59
|
+
node.arguments.length !== 0
|
|
60
|
+
) { return; }
|
|
61
|
+
|
|
62
|
+
const parent = node.parent;
|
|
63
|
+
const isExpressionStatement = parent.type === 'ExpressionStatement';
|
|
64
|
+
const isConciseEffectBody = parent === frame.callback &&
|
|
65
|
+
frame.callback.type === 'ArrowFunctionExpression' &&
|
|
66
|
+
frame.callback.expression;
|
|
67
|
+
|
|
68
|
+
if (isExpressionStatement || isConciseEffectBody) {
|
|
69
|
+
context.report({ node, messageId: 'preferSubscribeInEffect' });
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
};
|