kensington-eslint-plugin 0.4.0 → 0.5.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 +95 -26
- package/bin/check-reactive.js +611 -0
- package/index.js +21 -0
- package/package.json +11 -4
- package/rules/no-helper-function-trap.js +278 -0
- package/rules/no-out-of-scope-reactive-reference.js +220 -62
- package/rules/require-reactive-key.js +96 -0
package/README.md
CHANGED
|
@@ -42,6 +42,24 @@ export default [
|
|
|
42
42
|
];
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
The `strict` config opts in to maximum-safety reactive correctness. It extends `recommended`, promotes every reactive-correctness `warn` rule to `error`, and adds two extra rules:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
import kensington from 'kensington-eslint-plugin';
|
|
49
|
+
|
|
50
|
+
export default [
|
|
51
|
+
kensington.configs.strict,
|
|
52
|
+
// ...your other configs
|
|
53
|
+
];
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
What `strict` changes on top of `recommended`:
|
|
57
|
+
|
|
58
|
+
- **Adds `require-reactive-key`** (error). Paranoid mode. Flags every unkeyed `signal()`/`computed()`/`.transform()` call site, period. Not in `recommended` at any level. Keys are no-ops at module scope and required inside reactive scopes, so passing one always is safer than auditing call-site reachability. Suppress per call site with `eslint-disable-next-line kensington/require-reactive-key` when a top-level signal is known never to move into a reactive scope.
|
|
59
|
+
- **Promotes from `warn` to `error`**. `no-signal-async-write`, `no-ignored-effect-return`, `prefer-value-in-async`, `no-new-computed-in-computed`, `no-out-of-scope-reactive-reference`, `no-helper-function-trap`. All real reactive-correctness issues; strict mode chooses zero silent misses over tolerance of false positives.
|
|
60
|
+
|
|
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
|
+
|
|
45
63
|
The `style` config is opt-in and bundles the formatting rules at `warn` level:
|
|
46
64
|
|
|
47
65
|
```js
|
|
@@ -85,32 +103,34 @@ Because this is a standard ESLint plugin, it works anywhere ESLint runs with no
|
|
|
85
103
|
|
|
86
104
|
## Rules
|
|
87
105
|
|
|
88
|
-
| Rule | Description | Recommended |
|
|
89
|
-
|
|
90
|
-
| [`no-set-in-derivation`](#no-set-in-derivation) | Disallow `.set()` inside a `computed()` body or `.transform()` callback | error |
|
|
91
|
-
| [`no-self-read-write`](#no-self-read-write) | Disallow reading and writing the same signal in the same reactive run | error |
|
|
92
|
-
| [`no-set-on-derived-signal`](#no-set-on-derived-signal) | Disallow `.set()` on a derived (computed or transform) signal | error |
|
|
93
|
-
| [`no-new-signal-in-effect`](#no-new-signal-in-effect) | Disallow creating a new `signal()` inside an `effect()` body | error |
|
|
94
|
-
| [`no-effect-in-computed`](#no-effect-in-computed) | Disallow calling `effect()` inside a `computed()` body | error |
|
|
95
|
-
| [`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 |
|
|
96
|
-
| [`no-ignored-effect-return`](#no-ignored-effect-return) | Require capturing the return value of `effect()` inside a function | warn |
|
|
97
|
-
| [`prefer-value-in-async`](#prefer-value-in-async) | Prefer `.value` over `.get()` inside async callbacks within an `effect()` | warn |
|
|
98
|
-
| [`no-new-computed-in-effect`](#no-new-computed-in-effect) | Disallow creating a new `computed()` inside an `effect()` body | error |
|
|
99
|
-
| [`no-new-signal-in-computed`](#no-new-signal-in-computed) | Require a stable key for `signal()` calls inside a `computed()` body | error |
|
|
100
|
-
| [`no-unsafe-literal`](#no-unsafe-literal) | Disallow `.unsafeLiteral()` calls that bypass XSS protection | error |
|
|
101
|
-
| [`no-new-computed-in-computed`](#no-new-computed-in-computed) | Require a stable key for `computed()` and `.transform()` calls inside a `computed()` body | warn |
|
|
102
|
-
| [`no-out-of-scope-reactive-reference`](#no-out-of-scope-reactive-reference) | Disallow referencing a `signal()`, `computed()`, or `.transform()` from outside the computed scope where it was created | warn |
|
|
103
|
-
| [`no-effect-in-effect`](#no-effect-in-effect) | Disallow creating a new `effect()` inside an `effect()` body | error |
|
|
104
|
-
| [`no-async-effect`](#no-async-effect) | Disallow async callbacks passed to `effect()` | error |
|
|
105
|
-
| [`no-async-computed`](#no-async-computed) | Disallow async callbacks passed to `computed()` | error |
|
|
106
|
-
| [`
|
|
107
|
-
| [`
|
|
108
|
-
| [`prefer-
|
|
109
|
-
| [`prefer-
|
|
110
|
-
| [`prefer-
|
|
111
|
-
| [`
|
|
112
|
-
| [`
|
|
113
|
-
| [`
|
|
106
|
+
| Rule | Description | Recommended | Strict |
|
|
107
|
+
|------|-------------|-------------|--------|
|
|
108
|
+
| [`no-set-in-derivation`](#no-set-in-derivation) | Disallow `.set()` inside a `computed()` body or `.transform()` callback | error | error |
|
|
109
|
+
| [`no-self-read-write`](#no-self-read-write) | Disallow reading and writing the same signal in the same reactive run | error | error |
|
|
110
|
+
| [`no-set-on-derived-signal`](#no-set-on-derived-signal) | Disallow `.set()` on a derived (computed or transform) signal | error | error |
|
|
111
|
+
| [`no-new-signal-in-effect`](#no-new-signal-in-effect) | Disallow creating a new `signal()` inside an `effect()` body | error | error |
|
|
112
|
+
| [`no-effect-in-computed`](#no-effect-in-computed) | Disallow calling `effect()` inside a `computed()` body | error | error |
|
|
113
|
+
| [`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 |
|
|
114
|
+
| [`no-ignored-effect-return`](#no-ignored-effect-return) | Require capturing the return value of `effect()` inside a function | warn | error |
|
|
115
|
+
| [`prefer-value-in-async`](#prefer-value-in-async) | Prefer `.value` over `.get()` inside async callbacks within an `effect()` | warn | error |
|
|
116
|
+
| [`no-new-computed-in-effect`](#no-new-computed-in-effect) | Disallow creating a new `computed()` inside an `effect()` body | error | error |
|
|
117
|
+
| [`no-new-signal-in-computed`](#no-new-signal-in-computed) | Require a stable key for `signal()` calls inside a `computed()` body | error | error |
|
|
118
|
+
| [`no-unsafe-literal`](#no-unsafe-literal) | Disallow `.unsafeLiteral()` calls that bypass XSS protection | error | error |
|
|
119
|
+
| [`no-new-computed-in-computed`](#no-new-computed-in-computed) | Require a stable key for `computed()` and `.transform()` calls inside a `computed()` body | warn | error |
|
|
120
|
+
| [`no-out-of-scope-reactive-reference`](#no-out-of-scope-reactive-reference) | Disallow referencing a `signal()`, `computed()`, or `.transform()` from outside the computed scope where it was created | warn | error |
|
|
121
|
+
| [`no-effect-in-effect`](#no-effect-in-effect) | Disallow creating a new `effect()` inside an `effect()` body | error | error |
|
|
122
|
+
| [`no-async-effect`](#no-async-effect) | Disallow async callbacks passed to `effect()` | error | error |
|
|
123
|
+
| [`no-async-computed`](#no-async-computed) | Disallow async callbacks passed to `computed()` | error | error |
|
|
124
|
+
| [`no-helper-function-trap`](#no-helper-function-trap) | Require a stable key for `signal()`/`computed()`/`.transform()` inside helpers reachable from a reactive callback in the same file | warn | error |
|
|
125
|
+
| [`require-reactive-key`](#require-reactive-key) | Require a stable key on every `signal()`/`computed()`/`.transform()` call site, regardless of context | off | error |
|
|
126
|
+
| [`prefer-boolean-attribute-true`](#prefer-boolean-attribute-true) | Prefer `true` over `''` for boolean HTML attributes | style | style |
|
|
127
|
+
| [`prefer-camelcase-attrs`](#prefer-camelcase-attrs) | Prefer camelCase identifier keys over quoted kebab-case | style | style |
|
|
128
|
+
| [`prefer-style-object`](#prefer-style-object) | Prefer a `style` object over a CSS string | style | style |
|
|
129
|
+
| [`prefer-nested-attr-groups`](#prefer-nested-attr-groups) | Prefer nested form when attrs share a kebab prefix | style | style |
|
|
130
|
+
| [`prefer-array-for-multiline-content`](#prefer-array-for-multiline-content) | Require array brackets around multi-line tag content | style | style |
|
|
131
|
+
| [`attrs-on-call-line`](#attrs-on-call-line) | Attributes object must hug the tag call on both ends | style | style |
|
|
132
|
+
| [`attrs-canonical-shape`](#attrs-canonical-shape) | Attributes object must be inline or canonically stacked | style | style |
|
|
133
|
+
| [`consistent-content-layout`](#consistent-content-layout) | Tag content must hug the attrs `}` and the call's `)` | style | style |
|
|
114
134
|
|
|
115
135
|
---
|
|
116
136
|
|
|
@@ -468,6 +488,55 @@ effect(() => {
|
|
|
468
488
|
|
|
469
489
|
---
|
|
470
490
|
|
|
491
|
+
### `no-helper-function-trap`
|
|
492
|
+
|
|
493
|
+
Catches the call-stack version of the helper-function trap that the existing `no-new-signal-in-computed` and `no-new-computed-in-computed` rules miss. Those rules only flag lexical positions (the call is written directly inside a `computed(() => ...)` body in the source). This rule does single-file call-graph analysis. For every `signal()`/`computed()`/`.transform()` call without a key inside a named function, the rule checks whether that function is reachable (directly or transitively) from a reactive callback in the same file. Reactive callbacks recognized. function args to `computed(fn)`, `effect(fn)`, `signal.transform(fn)`, and `signal.mapWithKey(key, fn)`. Both inline arrow callbacks (`mapWithKey('id', x => row(x))`) and bare-identifier callbacks (`mapWithKey('id', row)`) are recognized.
|
|
494
|
+
|
|
495
|
+
```js
|
|
496
|
+
// Bad. row() is a plain helper, so signal() looks top-level in the source,
|
|
497
|
+
// but row() is called from inside mapWithKey's mapFn. The signal runs in the
|
|
498
|
+
// per-key computed at runtime.
|
|
499
|
+
function row(item) {
|
|
500
|
+
const highlight = signal(false);
|
|
501
|
+
return t.li({ class: highlight }, item.name);
|
|
502
|
+
}
|
|
503
|
+
const list = items.mapWithKey('id', item => row(item));
|
|
504
|
+
|
|
505
|
+
// Good. Key scopes the signal to the surrounding computed so the same
|
|
506
|
+
// instance is reused across re-runs.
|
|
507
|
+
function row(item) {
|
|
508
|
+
const highlight = signal(false, item.id);
|
|
509
|
+
return t.li({ class: highlight }, item.name);
|
|
510
|
+
}
|
|
511
|
+
const list = items.mapWithKey('id', item => row(item));
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Single-file analysis only. A helper defined in `cell.ts` and called from a reactive callback in `grid.ts` is NOT flagged by this rule on `cell.ts` (the call site is invisible). For cross-file coverage use `require-reactive-key`, which flags every unkeyed call site regardless of context.
|
|
515
|
+
|
|
516
|
+
False-positive surface. Helpers reachable from a reactive callback are flagged, even if they are ALSO called from non-reactive sites. The conservative choice is correct: if any call path enters a reactive scope, the key is needed.
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
### `require-reactive-key`
|
|
521
|
+
|
|
522
|
+
Paranoid mode. Flags every unkeyed `signal()`/`computed()`/`.transform()` call site, full stop. Keys are no-ops at module scope (the key argument is ignored when not inside a reactive scope) and required inside reactive scopes, so passing one always is safer than auditing call-site reachability.
|
|
523
|
+
|
|
524
|
+
```js
|
|
525
|
+
// Flagged. Pass a key.
|
|
526
|
+
const count = signal(0);
|
|
527
|
+
const doubled = computed(() => count.get() * 2);
|
|
528
|
+
const half = count.transform(v => v / 2);
|
|
529
|
+
|
|
530
|
+
// Suppress per call site if the call is genuinely top-level and you do not
|
|
531
|
+
// want the noise.
|
|
532
|
+
// eslint-disable-next-line kensington/require-reactive-key
|
|
533
|
+
const theme = signal('light');
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Off in the recommended config (too noisy for production codebases that legitimately scatter top-level signals). On in the `strict` config. Intended for agent-driven workflows, refactor-prone codebases, and projects that want maximum safety against later lifting code into a reactive callback.
|
|
537
|
+
|
|
538
|
+
---
|
|
539
|
+
|
|
471
540
|
### `prefer-boolean-attribute-true`
|
|
472
541
|
|
|
473
542
|
The HTML spec lists ~30 boolean attributes (`disabled`, `checked`, `hidden`, `selected`, etc.). Kensington treats `true` as "present" and `false`/`null`/`undefined` as "absent". An empty string is a confusing way to spell the same thing.
|