@thomaflette/eslint-plugin-solid-2 0.1.0 → 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.
@@ -0,0 +1,117 @@
1
+ # `solid/no-owned-scope-writes`
2
+
3
+ Disallow writing to signals/stores or calling actions inside component bodies and reactive compute
4
+ scopes.
5
+
6
+ This matches Solid 2's `SIGNAL_WRITE_IN_OWNED_SCOPE` behavior. In these places, derive values instead of writing state back into the graph.
7
+ It also matches beta.17's `ACTION_CALLED_IN_OWNED_SCOPE` error: actions belong at imperative
8
+ boundaries such as event handlers.
9
+
10
+ ## Bad
11
+
12
+ ```ts
13
+ createMemo(() => setCount(count() + 1));
14
+ ```
15
+
16
+ ```ts
17
+ createMemo(() => refresh());
18
+ ```
19
+
20
+ ```ts
21
+ createEffect(
22
+ () => {
23
+ setCount(count() + 1);
24
+ return count();
25
+ },
26
+ (value) => console.log(value),
27
+ );
28
+ ```
29
+
30
+ ```tsx
31
+ function Component() {
32
+ const [count, setCount] = createSignal(0);
33
+ setCount(1);
34
+ return <div>{count()}</div>;
35
+ }
36
+ ```
37
+
38
+ ```ts
39
+ const save = action(function* () {
40
+ yield api.save();
41
+ });
42
+ createMemo(() => save());
43
+ ```
44
+
45
+ ```tsx
46
+ function Component() {
47
+ const [state, setState] = createStore({ count: 0 });
48
+ setState((s) => {
49
+ s.count += 1;
50
+ });
51
+ return <div>{state.count}</div>;
52
+ }
53
+ ```
54
+
55
+ ## Good
56
+
57
+ ```ts
58
+ const doubled = createMemo(() => count() * 2);
59
+ ```
60
+
61
+ ```ts
62
+ createEffect(
63
+ () => count(),
64
+ (value) => {
65
+ setOther(value);
66
+ },
67
+ );
68
+ ```
69
+
70
+ ```tsx
71
+ function Component() {
72
+ const [count, setCount] = createSignal(0);
73
+ return <button onClick={() => setCount((c) => c + 1)}>Increment</button>;
74
+ }
75
+ ```
76
+
77
+ ```tsx
78
+ function Component() {
79
+ let button: HTMLButtonElement | undefined;
80
+ const [node, setNode] = createSignal<HTMLButtonElement | undefined>(undefined, {
81
+ ownedWrite: true,
82
+ });
83
+
84
+ createRenderEffect(() => {
85
+ setNode(button);
86
+ });
87
+
88
+ return <button ref={button}>{node() ? "Ready" : "Loading"}</button>;
89
+ }
90
+ ```
91
+
92
+ ## Notes
93
+
94
+ - Component bodies count as owned scopes.
95
+ - The compute phase of effects, memos, projections, and derived signal/store factories counts as
96
+ an owned scope. The apply phase (second argument) of `createEffect`/`createRenderEffect` does not.
97
+ - The 1.x single-callback form `createEffect(() => { ... })` is already deprecated in Solid 2 and is not flagged by this rule — the deprecation marker on the type is the relevant signal there.
98
+ - `ownedWrite: true` is respected for `createSignal`, including aliased imports.
99
+ - Actions created with `action(...)`, their `const` aliases, and immediately-invoked action
100
+ factories are detected.
101
+ - `refresh()` is a write-like graph operation and is rejected in the same owned scopes.
102
+ - Direct, aliased, and namespace `solid-js` imports are resolved by binding rather than by their
103
+ local spelling.
104
+
105
+ ## Options
106
+
107
+ ### `typescriptEnabled` (default `false`)
108
+
109
+ Component bodies are recognised by annotation (`Component`/…) or in-file `<C/>` usage by default.
110
+ Set `typescriptEnabled: true` to also detect components by their cross-file JSX usage and recognize
111
+ Solid factories, setters, actions, and `refresh` through re-export chains. Direct, aliased, and
112
+ namespace `solid-js` imports remain AST-only. The option is slower and requires ESLint type
113
+ information.
114
+
115
+ ```json
116
+ { "rules": { "solid/no-owned-scope-writes": ["error", { "typescriptEnabled": true }] } }
117
+ ```
@@ -0,0 +1,87 @@
1
+ # `solid/no-reactive-read-after-await`
2
+
3
+ Disallow reading a signal/memo accessor **after an `await`** inside an async reactive computation, where it is no longer tracked as a dependency.
4
+
5
+ Solid registers dependencies **synchronously**: when a computation runs, tracking is enabled, the compute function is called, and tracking is torn down the instant it returns. For an `async` compute function, that return happens at the **first `await`** — it hands back a promise and the tracking context is gone. Any accessor read in the continuation after the await runs untracked: it still returns a value, so nothing breaks loudly, but it never records a dependency, so the computation **won't re-run when that signal changes**.
6
+
7
+ Solid 2.0 has no dedicated `createAsync` primitive — the async primitive is an `async` compute function passed to `createMemo`, `createEffect`, `createRenderEffect`, or `createProjection`. This rule checks the compute callback (the first argument) of each.
8
+
9
+ ## Bad
10
+
11
+ ```ts
12
+ const [count, setCount] = createSignal(0);
13
+
14
+ const data = createMemo(async () => {
15
+ const res = await fetch(`/api?since=${lastSync}`);
16
+ return (await res.json()).filter((x) => x.id > count()); // ❌ count() after await — not tracked
17
+ });
18
+ ```
19
+
20
+ ```ts
21
+ createEffect(async () => {
22
+ await ready();
23
+ console.log(count()); // ❌ count() after await — the effect won't re-run when count changes
24
+ });
25
+ ```
26
+
27
+ ## Good
28
+
29
+ Read what you need **before** the first `await`, then use the captured value:
30
+
31
+ ```ts
32
+ const [count, setCount] = createSignal(0);
33
+
34
+ const data = createMemo(async () => {
35
+ const min = count(); // ✅ tracked
36
+ const res = await fetch(`/api?since=${lastSync}`);
37
+ return (await res.json()).filter((x) => x.id > min);
38
+ });
39
+ ```
40
+
41
+ If reading after the await is genuinely intentional (you do **not** want it to be a dependency), make that explicit with `untrack()`:
42
+
43
+ ```ts
44
+ createMemo(async () => {
45
+ await ready();
46
+ return untrack(() => count()); // ✅ explicitly untracked
47
+ });
48
+ ```
49
+
50
+ ## Why not a runtime warning?
51
+
52
+ In the browser there is no reliable way to detect this at runtime: once an async function yields at `await`, native `await` resumes through the engine's internal machinery (not `Promise.prototype.then`), and there is no `AsyncContext` yet — so a post-await read is indistinguishable from any other untracked read (an event handler, `untrack`). Static analysis sees the source directly, which is why this is a lint rule.
53
+
54
+ ## Type-aware mode (additive)
55
+
56
+ By default the rule is **AST-only**: it tracks accessors that come from a recognizable factory call
57
+ in the same file (`createSignal`/`createMemo`/`createOptimistic`, plus `const` aliases). Async compute
58
+ callbacks are recognized for effects, memos, projections, and derived signal/store factories,
59
+ including direct, aliased, and namespace `solid-js` imports. With type information enabled (the
60
+ `recommendedTypeChecked` config, or `{ typescriptEnabled: true }`), it additionally recognizes
61
+ accessors **by type** — a callee whose type is Solid's `Accessor`, and factory calls resolved by
62
+ symbol. This catches cases the AST path can't see:
63
+
64
+ ```ts
65
+ // member accessor — only caught with type info
66
+ function Row(props: { value: Accessor<number> }) {
67
+ return createMemo(async () => {
68
+ await fetch("/x");
69
+ return props.value(); // ❌ flagged under recommendedTypeChecked
70
+ });
71
+ }
72
+ ```
73
+
74
+ This is **purely additive** (ADR-0005): it never changes a verdict on code the AST path already handles, and it never reports more loosely — the type must be solid's `Accessor` (originating from `solid-js`/`@solidjs/signals`), so a plain `() => T` or a same-named non-solid `Accessor` is left alone. Both modes have **zero false positives**; type info only reduces false negatives.
75
+
76
+ ## Limitations
77
+
78
+ The "after an await" check is intentionally **sound over complete** — it never reports correct code, at the cost of missing some genuinely-buggy shapes (false negatives):
79
+
80
+ - An `await` reached only on one branch of an `if`/`switch`, or only on later iterations of a loop, is not treated as guaranteed, so a read after it is not flagged.
81
+ - Reads inside a nested helper closure are not analyzed — the rule can't know when the helper is invoked.
82
+ - Only signal/memo **accessors** are tracked. A store proxy read after an await is not covered (`createProjection` returns a store, so its _result_ isn't checked — but its async compute body is).
83
+
84
+ ## Notes
85
+
86
+ - Accessors aliased through a plain `const` (`const c = count`) are still caught; `untrack(() => count())` is allowed.
87
+ - The async **apply** callback of an effect (the second argument) is not checked here — its untracked reads are reported by [`no-untracked-read-in-effect-apply`](./no-untracked-read-in-effect-apply.md).
@@ -0,0 +1,157 @@
1
+ # `solid/no-stale-props-alias`
2
+
3
+ Disallow provable untracked reactive reads in component bodies and Solid control-flow function
4
+ children. This includes stale props aliases, direct props reads, local signal/memo accessor calls,
5
+ store reads, and accessor parameters supplied by control flow.
6
+
7
+ Solid 2 tracks prop reads when the property is read in JSX or another tracked scope. A top-level
8
+ alias reads once during component setup, outside tracking, so the alias can become stale.
9
+
10
+ The same execution rule applies to direct reads such as `console.log(props.name)` and to the
11
+ structure-building bodies of `For`, `Show`, and `Match` function children. JSX expressions,
12
+ reactive computations, nested event/helper closures, and explicit `untrack` calls are excluded.
13
+
14
+ ## Invalid
15
+
16
+ ```tsx
17
+ const Card: Component = (props) => {
18
+ const name = props.name;
19
+ return <h1>{name}</h1>;
20
+ };
21
+ ```
22
+
23
+ ```tsx
24
+ const Counter: Component = () => {
25
+ const [count] = createSignal(0);
26
+ console.log(count()); // direct component-body accessor read
27
+ return <span>{count()}</span>;
28
+ };
29
+ ```
30
+
31
+ ```tsx
32
+ <Show when={user()}>
33
+ {(user) => {
34
+ const name = user().name; // function-child accessor read outside JSX tracking
35
+ return <span>{name}</span>;
36
+ }}
37
+ </Show>
38
+ ```
39
+
40
+ ```tsx
41
+ const Card: Component = (props) => {
42
+ const userName = props.user.name;
43
+ return <h1>{userName}</h1>;
44
+ };
45
+ ```
46
+
47
+ ```tsx
48
+ const Card: Component = (props) => {
49
+ const name = props.name;
50
+ validate(name);
51
+ return <h1>{props.name}</h1>;
52
+ };
53
+ ```
54
+
55
+ ```tsx
56
+ const Card: Component = (props) => {
57
+ const name = props.name ?? "Anonymous";
58
+ return <h1>{name}</h1>;
59
+ };
60
+ ```
61
+
62
+ ```tsx
63
+ const Card: Component = (props) => {
64
+ const label = formatName(props.name);
65
+ return <h1>{label}</h1>;
66
+ };
67
+ ```
68
+
69
+ ```tsx
70
+ const Card: Component = (props) => {
71
+ const alias = props;
72
+ const name = alias.name; // the read through the alias is reported, not the alias itself
73
+ return <h1>{name}</h1>;
74
+ };
75
+ ```
76
+
77
+ ```tsx
78
+ const Card: Component = (props) => {
79
+ const copy = { ...props }; // spreading reads every property eagerly
80
+ return <h1>{copy.name}</h1>;
81
+ };
82
+ ```
83
+
84
+ ```tsx
85
+ const Card: Component = (props) => {
86
+ let name;
87
+ name = props.name;
88
+ return <h1>{name}</h1>;
89
+ };
90
+ ```
91
+
92
+ ## Valid
93
+
94
+ ```tsx
95
+ const Card: Component = (props) => {
96
+ return <h1>{props.name}</h1>;
97
+ };
98
+ ```
99
+
100
+ ```tsx
101
+ const Card: Component = (props) => {
102
+ const name = untrack(() => props.name);
103
+ return <h1>{name}</h1>;
104
+ };
105
+ ```
106
+
107
+ ```tsx
108
+ // The canonical defaults / rest patterns: merge and omit return reactive proxies, so passing the
109
+ // props object to them is a passthrough, not a read. Their results are tracked as props-like, so
110
+ // a later top-level read from them is still reported.
111
+ const Card: Component = (_props) => {
112
+ const props = merge({ size: "md" }, _props);
113
+ const rest = omit(props, "class");
114
+ return <div {...rest}>{props.size}</div>;
115
+ };
116
+ ```
117
+
118
+ ```tsx
119
+ // A whole-object alias performs no read; property reads through it in JSX stay reactive.
120
+ const Card: Component = (props) => {
121
+ const alias = props;
122
+ return <h1>{alias.name}</h1>;
123
+ };
124
+ ```
125
+
126
+ ## Options
127
+
128
+ ### `typescriptEnabled` (default `false`)
129
+
130
+ Component bodies are recognised by annotation (`Component`/…) or in-file `<C/>` usage by default.
131
+ Set `typescriptEnabled: true` to also detect components by their cross-file JSX usage via the
132
+ TypeScript type-checker, recognize nominal Solid accessors, and follow re-exported Solid
133
+ control-flow components (slower; requires ESLint type information).
134
+
135
+ ```json
136
+ { "rules": { "solid/no-stale-props-alias": ["warn", { "typescriptEnabled": true }] } }
137
+ ```
138
+
139
+ ## Notes
140
+
141
+ - A props report requires an _eager read_: a member access rooted at the props object
142
+ (`props.name`, `props[key]`, reads through a stable alias) or a spread of it (`{ ...props }`). A
143
+ bare `props` reference is never a read — passing the object to `merge`/`omit` or another helper
144
+ keeps reactivity intact. A helper that reads eagerly inside (`createForm(props)`) is a tolerated
145
+ false negative because its behavior is undecidable from the call site.
146
+ - Direct reads in nested functions and explicit `untrack(...)` calls are not reported.
147
+ - Reassignable props aliases are not followed.
148
+ - Destructured props are handled by `solid/no-destructure`.
149
+ - Function children of binding-proven Solid `For`, `Repeat`, `Show`, and `Match` elements are also
150
+ checked, including `children={fn}`, immutable function aliases, namespace imports, and nested
151
+ Solid control flow. JSX reads and explicit `untrack(...)` reads inside those callbacks remain
152
+ valid because they execute in tracked contexts.
153
+ - Callback parameters follow Solid 2's mode-specific semantics: default `For` indexes,
154
+ `keyed={false}` items, custom-key `For` items/indexes, and non-keyed `Show`/`Match` values are
155
+ accessors. Default `For` items, `Repeat` indexes, and keyed `Show`/`Match` values are raw.
156
+ - Type-aware mode additionally recognizes canonically typed Solid control-flow components that
157
+ arrive through re-export chains.
@@ -0,0 +1,97 @@
1
+ # `solid/no-untracked-read-in-effect-apply`
2
+
3
+ Disallow reading reactive state in a `createEffect` / `createRenderEffect` **apply** callback, which runs untracked.
4
+
5
+ An effect has two phases: **compute** (tracked — reads here record dependencies) and **apply** (untracked — the side effect). Reading reactive state in the apply phase triggers `STRICT_READ_UNTRACKED` in dev and won't re-run the effect. There are two ways to trip this, both reported by this rule:
6
+
7
+ 1. **Calling a signal/memo accessor** directly in the apply callback.
8
+ 2. **Reading a store proxy** that was passed through the compute return into the apply callback.
9
+
10
+ The fix for both: read what you need in the **compute** phase and use the passed value (or `untrack()` for signals, `deep()` for whole-store snapshots).
11
+
12
+ ## Bad
13
+
14
+ ```ts
15
+ const [count, setCount] = createSignal(0);
16
+ createEffect(
17
+ () => count(),
18
+ (value) => {
19
+ console.log(count());
20
+ }, // ❌ accessor called in apply
21
+ );
22
+ ```
23
+
24
+ ```ts
25
+ const [store] = createStore({ user: { name: "A" } });
26
+ createEffect(
27
+ () => store.user,
28
+ (user) => sendAnalytics(user.name), // ❌ store proxy read in apply
29
+ );
30
+ ```
31
+
32
+ ## Good
33
+
34
+ ```ts
35
+ createEffect(
36
+ () => count(),
37
+ (value) => {
38
+ console.log(value);
39
+ }, // ✅ use the passed value
40
+ );
41
+ ```
42
+
43
+ ```ts
44
+ createEffect(
45
+ () => ({ name: store.user.name }), // ✅ extract in compute
46
+ (value) => sendAnalytics(value.name),
47
+ );
48
+ ```
49
+
50
+ ```ts
51
+ createEffect(
52
+ () => deep(store), // ✅ deep() returns a plain snapshot safe to read in apply
53
+ (snapshot) => saveToLocalStorage(JSON.stringify(snapshot)),
54
+ );
55
+ ```
56
+
57
+ ```ts
58
+ createEffect(
59
+ () => count(),
60
+ (value) => {
61
+ // ✅ the handler runs later, on click — untracked reads are sanctioned there,
62
+ // and the runtime's STRICT_READ_UNTRACKED does not fire for it either
63
+ el.addEventListener("click", () => setOpen(!open()));
64
+ },
65
+ );
66
+ ```
67
+
68
+ ## Options
69
+
70
+ ### `typescriptEnabled` (default `false`)
71
+
72
+ The AST-only path recognises accessors created in the same file. Set `typescriptEnabled: true` to
73
+ also detect imported, parameter, and member accessors whose nominal type originates from Solid.
74
+ Structurally similar plain functions and readonly objects are not treated as accessors or stores.
75
+
76
+ ```json
77
+ {
78
+ "rules": {
79
+ "solid/no-untracked-read-in-effect-apply": ["warn", { "typescriptEnabled": true }]
80
+ }
81
+ }
82
+ ```
83
+
84
+ ## Notes
85
+
86
+ - Only reads that happen **directly** in the apply callback are reported — the read's nearest
87
+ enclosing function must be the apply callback itself. A read inside a closure created during
88
+ apply (an event handler, a `setInterval` callback) executes later, outside the apply phase, where
89
+ untracked reads are legitimate; flagging those was a false positive on sanctioned code. A closure
90
+ that is _invoked synchronously during apply_ (`arr.forEach(() => count())`) is a tolerated false
91
+ negative — the runtime diagnostic still covers it on executed paths.
92
+ - The `EffectBundle` form (`{ effect(value) { ... } }`) is checked the same way as a bare apply callback.
93
+ - Accessors aliased through a plain `const` (`const c = count`) are still caught; `untrack(() => count())` is allowed.
94
+ - Store proxy reports require proof: a bare store or a member path known from a literal initializer
95
+ to be an object/array. Computed paths and non-literal initializer shapes are left alone because
96
+ they may return primitives.
97
+ - This rule replaces the former `no-signal-in-effect-apply` and `no-store-proxy-in-effect-apply` rules (see [ADR-0006](./adr/0006-merge-leaf-owner-rules.md)).
@@ -0,0 +1,81 @@
1
+ # `solid/prefer-for`
2
+
3
+ Prefer Solid's `<For />` component over `Array#map(...)` when rendering JSX lists.
4
+
5
+ `<For />` matches Solid's list rendering model better than embedding `map(...)` inside JSX. In Solid 2's default `<For />`, the item callback parameter is a raw value (unlike `index`, which is an `Accessor<number>`), so the autofix leaves item reads untouched and only calls the index parameter.
6
+
7
+ ## Bad
8
+
9
+ ```tsx
10
+ <ul>
11
+ {items.map((item) => (
12
+ <li>{item.name}</li>
13
+ ))}
14
+ </ul>
15
+ ```
16
+
17
+ ```tsx
18
+ <For each={groups}>{(group) => group.items.map((item) => <Row item={item} />)}</For>
19
+ ```
20
+
21
+ ```tsx
22
+ <ol>
23
+ {props.data.map(({ text }) => (
24
+ <li>{text}</li>
25
+ ))}
26
+ </ol>
27
+ ```
28
+
29
+ ## Good
30
+
31
+ ```tsx
32
+ <ul>
33
+ <For each={items}>{(item) => <li>{item.name}</li>}</For>
34
+ </ul>
35
+ ```
36
+
37
+ ```tsx
38
+ <ol>
39
+ {props.data.map((item, i) => (
40
+ <li>
41
+ {i}: {item.name}
42
+ </li>
43
+ ))}
44
+ </ol>
45
+ // autofixable to (index is an accessor, so it is called):
46
+ <ol>
47
+ <For each={props.data}>
48
+ {(item, i) => (
49
+ <li>
50
+ {i()}: {item.name}
51
+ </li>
52
+ )}
53
+ </For>
54
+ </ol>
55
+ ```
56
+
57
+ ```tsx
58
+ <ol>{props.data.map((value) => value.text)}</ol>
59
+ ```
60
+
61
+ ## Notes
62
+
63
+ - The rule requires TypeScript to prove the receiver is an array. Syntax alone cannot distinguish
64
+ Array#map from an observable or another collection's `.map` method without false positives.
65
+ - Autofix only applies to arrow callbacks with at most the item and index parameters. Array's third
66
+ callback argument, normal functions (`arguments`/`this`), destructuring, and rest parameters are
67
+ reported without a fixer.
68
+ - The rule reports `map(...)` calls that produce JSX inside JSX, including when wrapped in a `{cond && …}` or `{cond ? … : …}` slot (those wrapped forms are reported but not autofixed).
69
+ - Destructured parameters, rest parameters, and other complex callback shapes are reported without a fixer.
70
+
71
+ ## Options
72
+
73
+ ### `typescriptEnabled` (default `false`)
74
+
75
+ Set `typescriptEnabled: true` to enable the rule. It reports only when the receiver is provably an
76
+ array and skips `Map`, `Set`, observables, `any`, and `unknown`. This is enabled by
77
+ `recommendedTypeChecked`; the AST-only `recommended` config leaves the rule off.
78
+
79
+ ```json
80
+ { "rules": { "solid/prefer-for": ["warn", { "typescriptEnabled": true }] } }
81
+ ```
@@ -0,0 +1,59 @@
1
+ # `solid/prefer-show`
2
+
3
+ Prefer Solid's `<Show />` component over JSX `&&` and ternary conditionals when rendering content.
4
+
5
+ `<Show />` makes conditional rendering explicit and aligns with Solid's control-flow components. Ternary cases can often be autofixed. `&&` cases are only reported because JSX and `<Show when={...}>` differ for falsy non-boolean values.
6
+
7
+ ## Bad
8
+
9
+ ```tsx
10
+ <div>{cond && <span>Content</span>}</div>
11
+ ```
12
+
13
+ ```tsx
14
+ <div>{cond ? <span>Content</span> : <span>Fallback</span>}</div>
15
+ ```
16
+
17
+ ```tsx
18
+ <For each={items}>{(item) => (item.ready ? <Ready /> : <Pending />)}</For>
19
+ ```
20
+
21
+ ## Good
22
+
23
+ ```tsx
24
+ <div>
25
+ <Show when={cond}>
26
+ <span>Content</span>
27
+ </Show>
28
+ </div>
29
+ ```
30
+
31
+ ```tsx
32
+ <div>
33
+ <Show when={cond} fallback={<span>Fallback</span>}>
34
+ <span>Content</span>
35
+ </Show>
36
+ </div>
37
+ ```
38
+
39
+ ```tsx
40
+ <For each={items}>
41
+ {(item) => (
42
+ <Show when={item.ready} fallback={<Pending />}>
43
+ <Ready />
44
+ </Show>
45
+ )}
46
+ </For>
47
+ ```
48
+
49
+ ```tsx
50
+ <Show when={cond}>Content</Show>
51
+ ```
52
+
53
+ ## Notes
54
+
55
+ - Only conditionals with a JSX branch are reported; plain value conditionals like `{a ? b : c}` or `{cond && value}` are left alone (rewriting them into `<Show>` would change behavior).
56
+ - Ternary expressions with a JSX branch are autofixable.
57
+ - Render callbacks are inspected whether written with a concise body (`(item) => cond ? <A/> : <B/>`) or a block body (`(item) => { return cond ? <A/> : <B/>; }`).
58
+ - `&&` expressions with JSX on the right are reported and offer the `<Show>` rewrite as an **editor suggestion**, never an autofix. `cond && <X/>` evaluates to the left operand when it is falsy, and Solid renders falsy-but-renderable values (`0`, `NaN`) as text — which `<Show>` would drop. A blanket `--fix` could therefore change behavior, so the rewrite is left for you to apply per occurrence after checking the condition's type.
59
+ - Example: `0 && <X />` renders `0` in JSX, but `<Show when={0}><X /></Show>` renders nothing. (Do **not** "promote" this suggestion to an autofix.)
@@ -0,0 +1,42 @@
1
+ # `solid/self-closing-comp`
2
+
3
+ Prefer self-closing syntax for empty JSX elements when allowed by the rule options.
4
+
5
+ This is a formatting/layout rule. It reports empty JSX elements that should be written as self-closing, or self-closing elements that should keep explicit closing tags based on the configured `html` and `component` options.
6
+
7
+ ## Bad
8
+
9
+ ```tsx
10
+ <div></div>
11
+ ```
12
+
13
+ ```tsx
14
+ <Button></Button>
15
+ ```
16
+
17
+ ## Good
18
+
19
+ ```tsx
20
+ <div />
21
+ ```
22
+
23
+ ```tsx
24
+ <Button />
25
+ ```
26
+
27
+ ```tsx
28
+ <div> </div>
29
+ ```
30
+
31
+ ## Notes
32
+
33
+ - **Why a stylistic rule lives in a correctness plugin:** this is the one deliberate scope
34
+ exception. It survives because it is _semantically inert_ (the report and both autofix
35
+ directions cannot change behavior, so the zero-FP/zero-corrupting-autofix bars are trivially
36
+ met), no formatter in the toolchain performs this normalization, and it was carried forward from
37
+ the original `eslint-plugin-solid` where users expect it. It is held to the same bars as every
38
+ other rule; it just guards style rather than correctness.
39
+ - `component` controls Solid/JSX components: `"all"` or `"none"`.
40
+ - `html` controls DOM elements: `"all"`, `"void"`, or `"none"`.
41
+ - Whitespace-only multiline children are treated as empty and can still be self-closed.
42
+ - Non-breaking spaces and explicit space expressions are treated as real children and are not self-closed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thomaflette/eslint-plugin-solid-2",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Sound ESLint rules for Solid 2 reactivity and idiomatic control flow.",
5
5
  "keywords": [
6
6
  "eslint",
@@ -21,7 +21,8 @@
21
21
  "url": "git+https://github.com/yumemi-thomas/eslint-plugin-solid-2.git"
22
22
  },
23
23
  "files": [
24
- "dist"
24
+ "dist",
25
+ "docs"
25
26
  ],
26
27
  "type": "module",
27
28
  "main": "./dist/index.cjs",
@@ -38,15 +39,15 @@
38
39
  "access": "public"
39
40
  },
40
41
  "dependencies": {
41
- "@typescript-eslint/utils": "^8.59.1"
42
+ "@typescript-eslint/utils": "^8.63.0"
42
43
  },
43
44
  "devDependencies": {
44
45
  "@types/node": "^25.6.0",
45
- "@typescript-eslint/parser": "^8.59.1",
46
+ "@typescript-eslint/parser": "^8.63.0",
46
47
  "@typescript/native": "npm:typescript@^7.0.2",
47
48
  "@typescript/native-preview": "7.0.0-dev.20260502.1",
48
49
  "bumpp": "^11.0.1",
49
- "eslint": "^10.3.0",
50
+ "eslint": "10.7.0",
50
51
  "solid-js": "2.0.0-beta.17",
51
52
  "typescript": "npm:@typescript/typescript6@^6.0.2",
52
53
  "vite-plus": "^0.1.20"
@@ -60,6 +61,7 @@
60
61
  },
61
62
  "scripts": {
62
63
  "build": "vp pack",
64
+ "bench": "vp test bench",
63
65
  "dev": "vp pack --watch",
64
66
  "test": "vp test",
65
67
  "check": "vp check"