kensington-eslint-plugin 0.1.2 → 0.2.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 +36 -0
- package/index.js +6 -0
- package/package.json +9 -2
- package/rules/no-set-in-transform.js +80 -0
- package/rules/no-set-on-transform.js +48 -0
package/README.md
CHANGED
|
@@ -70,6 +70,8 @@ Because this is a standard ESLint plugin, it works anywhere ESLint runs — no e
|
|
|
70
70
|
| [`no-effect-in-effect`](#no-effect-in-effect) | Disallow creating a new `effect()` inside an `effect()` body | error |
|
|
71
71
|
| [`no-async-effect`](#no-async-effect) | Disallow async callbacks passed to `effect()` | error |
|
|
72
72
|
| [`no-async-computed`](#no-async-computed) | Disallow async callbacks passed to `computed()` | error |
|
|
73
|
+
| [`no-set-in-transform`](#no-set-in-transform) | Disallow `.set()` inside a `.transform()` callback | error |
|
|
74
|
+
| [`no-set-on-transform`](#no-set-on-transform) | Disallow `.set()` on a transform-derived signal | error |
|
|
73
75
|
|
|
74
76
|
---
|
|
75
77
|
|
|
@@ -345,3 +347,37 @@ effect(() => {
|
|
|
345
347
|
fetch('/api').then(r => r.json()).then(v => data.set(v));
|
|
346
348
|
});
|
|
347
349
|
```
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
### `no-set-in-transform`
|
|
354
|
+
|
|
355
|
+
Transform callbacks must be pure derivations. Calling `.set()` inside one causes a write during a read pass, the same class of bug as `.set()` inside `computed()`.
|
|
356
|
+
|
|
357
|
+
```js
|
|
358
|
+
// Bad
|
|
359
|
+
const rows = items.transform(list => {
|
|
360
|
+
selectedId.set(null); // error
|
|
361
|
+
return list.map(item => t.li(item.name));
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Good — move the write into a separate effect
|
|
365
|
+
effect(() => {
|
|
366
|
+
if (!items.get().length) { selectedId.set(null); }
|
|
367
|
+
});
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
### `no-set-on-transform`
|
|
373
|
+
|
|
374
|
+
Transform-derived signals are read-only. Calling `.set()` on one throws at runtime; this rule catches it statically.
|
|
375
|
+
|
|
376
|
+
```js
|
|
377
|
+
// Bad
|
|
378
|
+
const doubled = count.transform(v => v * 2);
|
|
379
|
+
doubled.set(10); // error — transform results are read-only
|
|
380
|
+
|
|
381
|
+
// Good — write to the source signal instead
|
|
382
|
+
count.set(5);
|
|
383
|
+
```
|
package/index.js
CHANGED
|
@@ -13,6 +13,8 @@ import noNewComputedInComputed from './rules/no-new-computed-in-computed.js';
|
|
|
13
13
|
import noEffectInEffect from './rules/no-effect-in-effect.js';
|
|
14
14
|
import noAsyncEffect from './rules/no-async-effect.js';
|
|
15
15
|
import noAsyncComputed from './rules/no-async-computed.js';
|
|
16
|
+
import noSetInTransform from './rules/no-set-in-transform.js';
|
|
17
|
+
import noSetOnTransform from './rules/no-set-on-transform.js';
|
|
16
18
|
|
|
17
19
|
const plugin = {
|
|
18
20
|
meta: { name: 'eslint-plugin-kensington' },
|
|
@@ -32,6 +34,8 @@ const plugin = {
|
|
|
32
34
|
'no-effect-in-effect': noEffectInEffect,
|
|
33
35
|
'no-async-effect': noAsyncEffect,
|
|
34
36
|
'no-async-computed': noAsyncComputed,
|
|
37
|
+
'no-set-in-transform': noSetInTransform,
|
|
38
|
+
'no-set-on-transform': noSetOnTransform,
|
|
35
39
|
},
|
|
36
40
|
configs: {},
|
|
37
41
|
};
|
|
@@ -54,6 +58,8 @@ plugin.configs.recommended = {
|
|
|
54
58
|
'kensington/no-effect-in-effect': 'error',
|
|
55
59
|
'kensington/no-async-effect': 'error',
|
|
56
60
|
'kensington/no-async-computed': 'error',
|
|
61
|
+
'kensington/no-set-in-transform': 'error',
|
|
62
|
+
'kensington/no-set-on-transform': 'error',
|
|
57
63
|
},
|
|
58
64
|
};
|
|
59
65
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kensington-eslint-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "ESLint rules for kensington signal correctness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "node --test tests/no-set-in-computed.test.js tests/no-self-read-write.test.js tests/no-signal-async-write.test.js tests/no-set-on-computed.test.js tests/no-new-signal-in-effect.test.js tests/no-effect-in-computed.test.js tests/no-ignored-effect-return.test.js tests/prefer-value-in-async.test.js tests/no-new-computed-in-effect.test.js tests/no-new-signal-in-computed.test.js tests/no-unsafe-literal.test.js tests/no-new-computed-in-computed.test.js tests/no-effect-in-effect.test.js tests/no-async-effect.test.js tests/no-async-computed.test.js"
|
|
8
|
+
"test": "node --test tests/no-set-in-computed.test.js tests/no-self-read-write.test.js tests/no-signal-async-write.test.js tests/no-set-on-computed.test.js tests/no-new-signal-in-effect.test.js tests/no-effect-in-computed.test.js tests/no-ignored-effect-return.test.js tests/prefer-value-in-async.test.js tests/no-new-computed-in-effect.test.js tests/no-new-signal-in-computed.test.js tests/no-unsafe-literal.test.js tests/no-new-computed-in-computed.test.js tests/no-effect-in-effect.test.js tests/no-async-effect.test.js tests/no-async-computed.test.js tests/no-set-in-transform.test.js tests/no-set-on-transform.test.js"
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
11
|
"eslint": ">=9"
|
|
@@ -20,6 +20,13 @@
|
|
|
20
20
|
"type": "git",
|
|
21
21
|
"url": "https://github.com/ryanlsimms/kensington-eslint-plugin"
|
|
22
22
|
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"eslint",
|
|
25
|
+
"eslintplugin",
|
|
26
|
+
"eslint-plugin",
|
|
27
|
+
"kensington",
|
|
28
|
+
"signals"
|
|
29
|
+
],
|
|
23
30
|
"license": "ISC",
|
|
24
31
|
"files": [
|
|
25
32
|
"index.js",
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Reports .set() calls inside a .transform() callback.
|
|
2
|
+
// Stops searching when a nested effect() or computed() is reached (separate reactive context).
|
|
3
|
+
export default {
|
|
4
|
+
meta: {
|
|
5
|
+
type: 'problem',
|
|
6
|
+
docs: {
|
|
7
|
+
description: 'disallow .set() inside a .transform() callback',
|
|
8
|
+
},
|
|
9
|
+
messages: {
|
|
10
|
+
noSetInTransform:
|
|
11
|
+
'.set() called inside a .transform() callback. Transform functions must be pure derivations. ' +
|
|
12
|
+
'Move the write into a separate effect() instead.',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
create(context) {
|
|
17
|
+
const computedNames = new Set();
|
|
18
|
+
const effectNames = new Set();
|
|
19
|
+
// Each entry is 'transform', 'computed', 'effect', or 'other' — innermost frame is last.
|
|
20
|
+
const fnStack = [];
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
ImportDeclaration(node) {
|
|
24
|
+
if (node.source.value !== 'kensington') { return; }
|
|
25
|
+
for (const spec of node.specifiers) {
|
|
26
|
+
if (spec.type !== 'ImportSpecifier') { continue; }
|
|
27
|
+
if (spec.imported.name === 'computed') { computedNames.add(spec.local.name); }
|
|
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 (parent.type === 'CallExpression' && parent.arguments[0] === node) {
|
|
35
|
+
if (
|
|
36
|
+
parent.callee.type === 'MemberExpression' &&
|
|
37
|
+
parent.callee.property.type === 'Identifier' &&
|
|
38
|
+
parent.callee.property.name === 'transform'
|
|
39
|
+
) {
|
|
40
|
+
fnStack.push('transform');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (parent.callee.type === 'Identifier') {
|
|
44
|
+
if (computedNames.has(parent.callee.name)) {
|
|
45
|
+
fnStack.push('computed');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (effectNames.has(parent.callee.name)) {
|
|
49
|
+
fnStack.push('effect');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
fnStack.push('other');
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
':matches(ArrowFunctionExpression, FunctionExpression):exit'() {
|
|
58
|
+
fnStack.pop();
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
CallExpression(node) {
|
|
62
|
+
if (
|
|
63
|
+
node.callee.type !== 'MemberExpression' ||
|
|
64
|
+
node.callee.object.type !== 'Identifier' ||
|
|
65
|
+
node.callee.property.type !== 'Identifier' ||
|
|
66
|
+
node.callee.property.name !== 'set' ||
|
|
67
|
+
node.arguments.length !== 1
|
|
68
|
+
) { return; }
|
|
69
|
+
|
|
70
|
+
for (let i = fnStack.length - 1; i >= 0; i--) {
|
|
71
|
+
if (fnStack[i] === 'transform') {
|
|
72
|
+
context.report({ node, messageId: 'noSetInTransform' });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (fnStack[i] === 'effect' || fnStack[i] === 'computed') { return; }
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Reports .set() on a variable directly assigned from a .transform() call.
|
|
2
|
+
// Transform-derived signals are read-only; kensington throws at runtime, but this catches it statically.
|
|
3
|
+
export default {
|
|
4
|
+
meta: {
|
|
5
|
+
type: 'problem',
|
|
6
|
+
docs: {
|
|
7
|
+
description: 'disallow .set() on a transform-derived signal',
|
|
8
|
+
},
|
|
9
|
+
messages: {
|
|
10
|
+
noSetOnTransform:
|
|
11
|
+
"'{{name}}' is a transform-derived signal and cannot be written with .set(). " +
|
|
12
|
+
'Use signal() for writable state.',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
create(context) {
|
|
17
|
+
const transformBindings = new Set();
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
VariableDeclarator(node) {
|
|
21
|
+
if (
|
|
22
|
+
node.id.type !== 'Identifier' ||
|
|
23
|
+
!node.init ||
|
|
24
|
+
node.init.type !== 'CallExpression' ||
|
|
25
|
+
node.init.callee.type !== 'MemberExpression' ||
|
|
26
|
+
node.init.callee.property.type !== 'Identifier' ||
|
|
27
|
+
node.init.callee.property.name !== 'transform'
|
|
28
|
+
) { return; }
|
|
29
|
+
transformBindings.add(node.id.name);
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
CallExpression(node) {
|
|
33
|
+
if (
|
|
34
|
+
node.callee.type !== 'MemberExpression' ||
|
|
35
|
+
node.callee.object.type !== 'Identifier' ||
|
|
36
|
+
node.callee.property.type !== 'Identifier' ||
|
|
37
|
+
node.callee.property.name !== 'set' ||
|
|
38
|
+
node.arguments.length !== 1
|
|
39
|
+
) { return; }
|
|
40
|
+
|
|
41
|
+
const name = node.callee.object.name;
|
|
42
|
+
if (transformBindings.has(name)) {
|
|
43
|
+
context.report({ node, messageId: 'noSetOnTransform', data: { name } });
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|