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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kensington-eslint-plugin",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "ESLint rules for kensington signal correctness",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -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
+ };