@thomaflette/eslint-plugin-solid-2 0.1.0 → 0.2.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 +8 -3
- package/dist/index.cjs +810 -468
- package/dist/index.d.cts +79 -63
- package/dist/index.d.mts +79 -63
- package/dist/index.mjs +810 -468
- package/docs/README.md +24 -0
- package/docs/adr/0001-only-false-positive-free-rules.md +65 -0
- package/docs/adr/0002-sound-component-detection.md +66 -0
- package/docs/adr/0003-require-solid-binding.md +50 -0
- package/docs/adr/0004-correctness-guard-not-migration-tool.md +48 -0
- package/docs/adr/0005-recommended-type-checked-config.md +27 -0
- package/docs/adr/0006-merge-leaf-owner-rules.md +42 -0
- package/docs/adr/0007-typescript-audience.md +35 -0
- package/docs/components-return-once.md +89 -0
- package/docs/dev-diagnostic-coverage.md +29 -0
- package/docs/jsx-no-duplicate-props.md +34 -0
- package/docs/no-destructure.md +92 -0
- package/docs/no-leaf-owner-operations.md +90 -0
- package/docs/no-owned-scope-writes.md +117 -0
- package/docs/no-reactive-read-after-await.md +87 -0
- package/docs/no-stale-props-alias.md +157 -0
- package/docs/no-untracked-read-in-effect-apply.md +97 -0
- package/docs/prefer-for.md +81 -0
- package/docs/prefer-show.md +59 -0
- package/docs/self-closing-comp.md +42 -0
- package/package.json +7 -5
package/docs/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Rule documentation
|
|
2
|
+
|
|
3
|
+
Each rule page explains the Solid 2 behavior it enforces, with examples and configuration details.
|
|
4
|
+
|
|
5
|
+
## Reactivity and ownership
|
|
6
|
+
|
|
7
|
+
- [components-return-once](./components-return-once.md)
|
|
8
|
+
- [no-destructure](./no-destructure.md)
|
|
9
|
+
- [no-leaf-owner-operations](./no-leaf-owner-operations.md)
|
|
10
|
+
- [no-owned-scope-writes](./no-owned-scope-writes.md)
|
|
11
|
+
- [no-reactive-read-after-await](./no-reactive-read-after-await.md)
|
|
12
|
+
- [no-stale-props-alias](./no-stale-props-alias.md)
|
|
13
|
+
- [no-untracked-read-in-effect-apply](./no-untracked-read-in-effect-apply.md)
|
|
14
|
+
|
|
15
|
+
## JSX and control flow
|
|
16
|
+
|
|
17
|
+
- [jsx-no-duplicate-props](./jsx-no-duplicate-props.md)
|
|
18
|
+
- [prefer-for](./prefer-for.md)
|
|
19
|
+
- [prefer-show](./prefer-show.md)
|
|
20
|
+
- [self-closing-comp](./self-closing-comp.md)
|
|
21
|
+
|
|
22
|
+
## Design records
|
|
23
|
+
|
|
24
|
+
The [ADRs](./adr) document the plugin's false-positive policy, component detection strategy, and configuration decisions.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Ship only false-positive-free rules; lean on tsc and runtime diagnostics
|
|
2
|
+
|
|
3
|
+
**Context.** This is a brand-new Solid 2 plugin with no track record of bug
|
|
4
|
+
reports, so we judge rules by _design-level_ false-positive risk, not by filed
|
|
5
|
+
issues. Solid 2 also ships dev-mode runtime diagnostics (`STRICT_READ_UNTRACKED`,
|
|
6
|
+
`SIGNAL_WRITE_IN_OWNED_SCOPE`, cleanup/flush/forbidden-scope errors, …) and the
|
|
7
|
+
plugin targets TypeScript codebases where `tsc` is always running.
|
|
8
|
+
|
|
9
|
+
**Decision.** A rule survives only if it has **zero false positives** (it may
|
|
10
|
+
have false negatives — silently missing bad code is acceptable; lying about good
|
|
11
|
+
code is not). We act on a rule's false positives by _cause_:
|
|
12
|
+
|
|
13
|
+
- **Fix** it when the FP comes from a fixable implementation flaw (a crude
|
|
14
|
+
heuristic, a lagging allowlist, an unsafe autofix) _and_ it guards real value
|
|
15
|
+
that neither `tsc` nor the runtime covers.
|
|
16
|
+
- **Delete** it (rule + tests + docs + config entry) when the FP is _fundamental_
|
|
17
|
+
(asks a statically undecidable question) or when `tsc` already flags the same
|
|
18
|
+
code in a TS codebase. "Disable in recommended" is not used.
|
|
19
|
+
|
|
20
|
+
The plugin is **TypeScript-only** (we assume `tsc` runs) and every rule works
|
|
21
|
+
from the AST/scope alone by default, so the default ruleset runs on **both oxlint
|
|
22
|
+
and ESLint** (oxlint's plugin API has no type-checker). Type-aware analysis,
|
|
23
|
+
where it helps, is **explicit and opt-in** via a per-rule `typescriptEnabled`
|
|
24
|
+
option (off by default, documented as slower) and is **purely additive** — it
|
|
25
|
+
finds more, never flips a verdict on correct code. See
|
|
26
|
+
[ADR-0002](./0002-sound-component-detection.md) (component detection) and
|
|
27
|
+
`prefer-for` (array-receiver check) for the rules that offer it.
|
|
28
|
+
|
|
29
|
+
There is no exception for security-review heuristics. `no-innerhtml` and
|
|
30
|
+
`jsx-no-script-url` were removed because sanitized HTML and author-defined URL-like
|
|
31
|
+
props are valid code that static analysis cannot prove safe or unsafe.
|
|
32
|
+
|
|
33
|
+
## Considered options
|
|
34
|
+
|
|
35
|
+
- _Disable FP-prone rules in `recommended` but keep them shippable_ — rejected:
|
|
36
|
+
the default experience is where false positives hurt adoption, and we prefer a
|
|
37
|
+
small, trustworthy surface to a large, opt-out one.
|
|
38
|
+
- _Keep all rules and accept some FPs_ — rejected: a linter that flags correct
|
|
39
|
+
code trains users to disable it.
|
|
40
|
+
|
|
41
|
+
## Consequences
|
|
42
|
+
|
|
43
|
+
- Deleted (fundamental FP): the original heuristic `no-untracked-reactive-read`,
|
|
44
|
+
`no-async-outside-loading-boundary`, `no-innerhtml`, `jsx-no-script-url`.
|
|
45
|
+
- Deleted (tsc-subsumed): `jsx-no-undef`, `jsx-uses-vars`, `no-react-deps`,
|
|
46
|
+
`no-invalid-cleanup-return`, `no-array-handlers` (the `[handler, data]` tuple
|
|
47
|
+
is valid Solid 2 and `tsc` type-checks handlers), `no-unknown-namespaces`,
|
|
48
|
+
`accessor-as-prop-value`, `no-dynamic-keyed`, and duplicate JSX-attribute detection.
|
|
49
|
+
- Verified empirically against Solid 2 `beta.17` types (tsgo): deletions fire on
|
|
50
|
+
correct code; survivors don't. `no-unknown-namespaces` is tsc-subsumed —
|
|
51
|
+
Solid 2's JSX types have no general namespace index signature (`prop:` needs
|
|
52
|
+
`ExplicitProperties` augmentation, the `class:${string}` index is commented
|
|
53
|
+
out, there is no `Directives`/`on:` mechanism), so `tsc` rejects every
|
|
54
|
+
removed/unknown/style/component-namespaced attribute the rule flags. The DOM
|
|
55
|
+
RFC (dom.md) also removed the `on:` namespace, which the rule still lists as
|
|
56
|
+
allowed — so it is stale as well as redundant.
|
|
57
|
+
- A narrower strict-read analysis now lives behind `no-stale-props-alias`: it reports only reads
|
|
58
|
+
proven from bindings or nominal Solid accessor types, and recognizes JSX, nested closures,
|
|
59
|
+
reactive callbacks, and `untrack` as safe execution contexts. It does not restore the deleted
|
|
60
|
+
heuristic's guesses about arbitrary helper calls.
|
|
61
|
+
- Runtime-state mirrors for `PENDING_ASYNC_FORBIDDEN_SCOPE`, `SETTLED_CLEANUP_UNOWNED`, and the
|
|
62
|
+
`NO_OWNER_*` diagnostics were prototyped and rejected. Whether they fire depends on the source's
|
|
63
|
+
pending state or the active owner at execution; an owner-backed scheduler and an already-settled
|
|
64
|
+
async memo are valid counterexamples to static callback/source heuristics. Solid's exact runtime
|
|
65
|
+
diagnostics own these cases.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Sound component detection, with opt-in type-aware coverage
|
|
2
|
+
|
|
3
|
+
**Context.** Several rules (`components-return-once`, `no-owned-scope-writes`,
|
|
4
|
+
`no-destructure`) need to know "is this function a Solid component?" The original
|
|
5
|
+
`isComponentLike` guessed _returns JSX and isn't lowercase-named_ — not sound: a
|
|
6
|
+
JSX-returning helper called imperatively (`const cell = RenderRow(data)`) matched
|
|
7
|
+
it and was falsely flagged. An earlier attempt made this two-tier with
|
|
8
|
+
_automatic_ type-aware suppression, but that produced **corrective variance** —
|
|
9
|
+
the AST-only result was a false positive that type info silently removed, so the
|
|
10
|
+
same code was flagged under oxlint but clean under type-aware ESLint. Type-aware
|
|
11
|
+
analysis also has a real cost (it forces a TS `Program`) that shouldn't be
|
|
12
|
+
incurred silently.
|
|
13
|
+
|
|
14
|
+
**Decision.** Detect components only by **sound** signals — never the leaky
|
|
15
|
+
capitalized-JSX guess — and make any type-checker use **explicit and opt-in**:
|
|
16
|
+
|
|
17
|
+
- **Default (AST/scope only, no type-checker):** a function is a component iff it
|
|
18
|
+
is annotated `Component`/`VoidComponent`/`ParentComponent`/`FlowComponent`, or
|
|
19
|
+
is used as `<C/>` in the same file. Zero false positives. Runs on oxlint.
|
|
20
|
+
- **`typescriptEnabled: true` (per-rule option, off by default):** _additionally_
|
|
21
|
+
treat a function as a component when its symbol is used as a JSX tag anywhere
|
|
22
|
+
in the program (catches exported/cross-file components). Requires ESLint type
|
|
23
|
+
information and is slower; documented as such.
|
|
24
|
+
|
|
25
|
+
The type-aware path is **purely additive** — it only ever finds _more_ real
|
|
26
|
+
components, never flips a verdict on correct code. So there is no corrective
|
|
27
|
+
variance: behaviour is consistent and zero-FP in every environment; the option
|
|
28
|
+
only buys extra coverage at a cost the user opts into.
|
|
29
|
+
|
|
30
|
+
## Consequences
|
|
31
|
+
|
|
32
|
+
- Detection is exposed from `analysis/component-recognition.ts` as a self-indexing
|
|
33
|
+
`isComponent(node, context)`: it builds the in-file
|
|
34
|
+
`<C/>` usage index once per source (memoized on `SourceCode`) and reads `typescriptEnabled` from
|
|
35
|
+
`context` itself. Because that index is complete on first query, rules call it **inline during
|
|
36
|
+
traversal** — no rule threads a `jsxComponentNames` set or defers detection to `Program:exit`.
|
|
37
|
+
Do not push the index back onto callers.
|
|
38
|
+
- The in-file `<C/>` index is keyed by **binding, not name** (`getInFileComponentVariables`): each
|
|
39
|
+
_direct_ identifier tag is resolved to the variable it references, so a local helper whose name
|
|
40
|
+
collides with an imported component is not conflated with it. Host elements (lowercase tags) and
|
|
41
|
+
member-tag roots (`<Foo.Bar/>` renders `Foo.Bar`, not `Foo`) are excluded — a function is detected
|
|
42
|
+
only when used as a bare `<Fn/>`. (An earlier name-string index false-positived on helpers named
|
|
43
|
+
like host elements — `title`, `summary` — or like an imported component.)
|
|
44
|
+
- **Tolerated false negative (member tags):** a component rendered _only_ via a compound/namespaced
|
|
45
|
+
tag (`const Tabs = { Panel }; <Tabs.Panel/>`) is not detected by the default tier — excluding
|
|
46
|
+
member-tag roots is what removes the `<Theme.Provider/>` false positive (a config factory named
|
|
47
|
+
`Theme` is not a component). Zero-FP wins over this FN (ADR-0001); `typescriptEnabled` resolves the
|
|
48
|
+
member-tag symbol and still detects it.
|
|
49
|
+
- The annotation signal resolves the type's **binding**, not just its name: a
|
|
50
|
+
function counts as a component only when `Component`/`VoidComponent`/… resolves
|
|
51
|
+
to a `solid-js` import (or an unresolved/ambient global), never a same-named
|
|
52
|
+
local `type`/`interface` or an import from another package. (The original
|
|
53
|
+
implementation matched the bare type name, which false-positived on a
|
|
54
|
+
user-defined `Component` type — an FP audit caught it; the annotation proof in
|
|
55
|
+
`analysis/component-recognition.ts` now enforces the binding, mirroring ADR-0003.)
|
|
56
|
+
- **Tolerated false negative:** a bare, unannotated component used only in another
|
|
57
|
+
file is not detected by default. Close it by annotating it `Component<P>`,
|
|
58
|
+
using it as `<C/>` in-file, or enabling `typescriptEnabled`. Each rule's tests
|
|
59
|
+
include an explicit valid case documenting this, so the gap is visible.
|
|
60
|
+
- Do **not** restore the capitalized-JSX heuristic — it reintroduces the false
|
|
61
|
+
positive and the corrective variance this decision removes.
|
|
62
|
+
- `typescript` remains a peer dependency, used only by the opt-in path.
|
|
63
|
+
- The original heuristic `no-untracked-reactive-read` stays deleted: sound component detection is
|
|
64
|
+
necessary but not sufficient. The narrower analyzer used by `no-stale-props-alias` additionally
|
|
65
|
+
proves the reactive binding and execution context; intentional one-time reads use explicit
|
|
66
|
+
`untrack`, matching the Solid 2 strict-read contract.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Match Solid APIs by binding, not by bare name
|
|
2
|
+
|
|
3
|
+
**Context.** The reactive-scope rules (`no-cleanup`/`no-flush`/`no-primitives-in-forbidden-scope`,
|
|
4
|
+
`no-signal-in-effect-apply`) detected both the enclosing scope (`createTrackedEffect`, `onSettled`,
|
|
5
|
+
`createEffect`) and the forbidden call (`flush`, `onCleanup`, a primitive factory) by **bare name**
|
|
6
|
+
(`canonicalNames.has(name)`), with no check that the name actually came from `solid-js`. That
|
|
7
|
+
false-positives on correct code that uses a same-named function from elsewhere — e.g. a stream
|
|
8
|
+
library's `flush`, a state library's `createStore`, or a locally-defined `onCleanup` — called inside
|
|
9
|
+
a genuine Solid scope. `flush`/`createStore`/`onCleanup` are common names, so this was real. (Verified
|
|
10
|
+
by running the rules.) `resolveSolidCallee` had the same latent bug via an early bare-name fallback.
|
|
11
|
+
|
|
12
|
+
**Decision.** Resolve the callee's binding before treating it as a Solid API (the shared resolver
|
|
13
|
+
and binding-fact index live in `analysis/solid-bindings.ts`). A name counts as the Solid API only when it
|
|
14
|
+
is a `solid-js` import (direct or aliased) **or** an unresolved global (auto-import). A name that
|
|
15
|
+
binds to a local declaration or an import from another package is **not** the Solid API.
|
|
16
|
+
`resolveSolidCallee` was tightened the same way and also handles namespace members and immutable
|
|
17
|
+
`const` alias chains.
|
|
18
|
+
|
|
19
|
+
## Consequences
|
|
20
|
+
|
|
21
|
+
- The four rules (plus `no-store-proxy-in-effect-apply`, which uses `resolveSolidCallee`) no longer
|
|
22
|
+
false-positive on same-named non-Solid functions.
|
|
23
|
+
- Genuinely-correct unresolved usage (`createTrackedEffect(() => flush())` with no import, relying on
|
|
24
|
+
auto-import/globals) still fires — we assume an unresolved canonical name is the Solid one.
|
|
25
|
+
- This relies only on scope analysis (no type-checker), so it still runs under oxlint.
|
|
26
|
+
- Rules with a `typescriptEnabled` path additionally use canonical TypeScript symbols to follow
|
|
27
|
+
Solid APIs through re-export chains; this remains additive to direct binding resolution.
|
|
28
|
+
- Residual false negatives remain by design (a call indirected through a renamed local variable, or
|
|
29
|
+
reached via a nested helper) — tolerated per [ADR-0001](./0001-only-false-positive-free-rules.md).
|
|
30
|
+
- **Resolution is self-contained (no caller-built alias index).** The shared source-level analysis
|
|
31
|
+
resolves direct, namespace, and `const`-aliased `solid-js` imports, so the per-rule alias `Set`s
|
|
32
|
+
and `ImportDeclaration` handlers that earlier fed a `(aliases, canonicalNames)` pair were redundant
|
|
33
|
+
and were removed; `bindsToSolid`/`resolveSolidCallee`/`isSolidApiCallbackArgument` now take only
|
|
34
|
+
`(identifier, context, canonicalNames)`. `resolveSolidCallee` returns the **canonical** name even
|
|
35
|
+
for an aliased import. The dead `collectSolidAliases` / `matchesSolidName` / `isCallbackArgumentOf`
|
|
36
|
+
helpers were deleted. Do not reintroduce a caller-side alias index "for speed" — `trace` is the
|
|
37
|
+
single resolution path. Reactive accessor/action/setter/store facts are indexed once per source
|
|
38
|
+
and reused by all consumers.
|
|
39
|
+
- **The bare-canonical-name fallback fires only for a _truly unresolved_ global.** `trace` returns
|
|
40
|
+
the identifier unchanged both for an auto-import global _and_ for a local the tracer can't follow
|
|
41
|
+
(a `let`, a parameter, a destructured `const { onCleanup } = lib`). `resolveSolidCallee` therefore
|
|
42
|
+
re-checks with `findVariable`: a name that binds to any local declaration is the user's own and
|
|
43
|
+
resolves to null; only a name with no binding (or a defs-less global) is treated as the Solid API.
|
|
44
|
+
This closes a class of false positives on locally-named `flush`/`onCleanup`/`createSignal`.
|
|
45
|
+
- **All three detection paths are now binding-based, not name-based** (the point of this ADR, now
|
|
46
|
+
applied uniformly): the callee (`resolveSolidCallee`), the component **type annotation**
|
|
47
|
+
(`typeNameBindsToSolid`, see [ADR-0002](./0002-sound-component-detection.md)), and **component
|
|
48
|
+
usage** (`getInFileComponentVariables` resolves each `<C/>` tag to the variable it references and
|
|
49
|
+
skips host elements / member-tag roots, so a local helper whose name merely collides with an
|
|
50
|
+
imported component is never misclassified).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# A Solid 2.0 correctness guard, not a migration tool; refined deletion criterion and fix tiers
|
|
2
|
+
|
|
3
|
+
**Context.** It was tempting to give `@thomaflette/eslint-plugin-solid-2` a second mandate:
|
|
4
|
+
help people port 1.x→2.0 with rules that flag renamed/moved/removed APIs
|
|
5
|
+
(`solid-js/store → solid-js`, `createResource`, `Index`, `classList`, …). We
|
|
6
|
+
considered a separate `migration` config of such codemod rules. On reflection we
|
|
7
|
+
rejected it: the plugin's job is to **stop people writing incorrect Solid 2.0
|
|
8
|
+
code**, not to narrate what changed since 1.x. "This import moved" / "this API
|
|
9
|
+
was renamed" is historical nagging, and `tsc` already errors when a removed
|
|
10
|
+
symbol doesn't exist. A migration codemod is a separate, one-time tool, not a
|
|
11
|
+
linter's standing job.
|
|
12
|
+
|
|
13
|
+
Two refinements surfaced while reasoning this through and _do_ survive, because
|
|
14
|
+
they govern the correctness rules we keep.
|
|
15
|
+
|
|
16
|
+
**Decision.**
|
|
17
|
+
|
|
18
|
+
1. **No migration config.** The plugin only detects code that is _wrong in 2.0_
|
|
19
|
+
— regardless of whether the author is migrating or writing fresh. It never
|
|
20
|
+
reports that an API moved or was renamed.
|
|
21
|
+
|
|
22
|
+
2. **Deletion criterion.** A rule is deleted when `tsc` already reports the
|
|
23
|
+
same problem. An autofix does not justify duplicate diagnostics, especially
|
|
24
|
+
when the fix can fail for unions or otherwise change valid code.
|
|
25
|
+
|
|
26
|
+
3. **Three fix tiers** — zero-FP extends to "zero-corrupting-autofix":
|
|
27
|
+
- **Safe autofix** (`meta.fixable`, applied by `--fix`) — only for rewrites
|
|
28
|
+
that cannot change behavior (for example ternary→`<Show>`).
|
|
29
|
+
- **Suggestion** (`meta.hasSuggestions`) — a concrete rewrite that _might_
|
|
30
|
+
change behavior; offered one-click in the editor, reviewed per occurrence,
|
|
31
|
+
never run by `--fix`. Home for `&&`→`<Show>`.
|
|
32
|
+
- **Report-only** — when no single mechanical rewrite exists.
|
|
33
|
+
|
|
34
|
+
**Correctness rules added/affirmed under this scope:**
|
|
35
|
+
|
|
36
|
+
- `prefer-for` remains essential idiomatic-Solid guidance, but is enabled only
|
|
37
|
+
with type information so Array#map is proven before a report.
|
|
38
|
+
- `prefer-show` `&&` case: report-only or suggestion, never a safe autofix —
|
|
39
|
+
`cond && <X/>` renders falsy-but-renderable values (`0`, `NaN`) that
|
|
40
|
+
`<Show>` would drop, so a mechanical rewrite changes behavior.
|
|
41
|
+
|
|
42
|
+
## Consequences
|
|
43
|
+
|
|
44
|
+
- `recommended` keeps its trust property: a small, zero-FP correctness surface.
|
|
45
|
+
- Porting help is explicitly out of scope — point migrators at a codemod, not
|
|
46
|
+
this plugin.
|
|
47
|
+
- See [ADR-0005](./0005-recommended-type-checked-config.md) for how the
|
|
48
|
+
type-aware correctness rules are exposed.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Expose type-aware correctness rules via a `recommendedTypeChecked` config
|
|
2
|
+
|
|
3
|
+
**Context.** Per ADR-0002, the highest-value detection (cross-file component
|
|
4
|
+
detection, nominal accessor reads, and sound Array#map identification) needs the type-checker,
|
|
5
|
+
but type info is currently reachable only through a per-rule `typescriptEnabled`
|
|
6
|
+
flag that most users will never discover. Meanwhile every default rule is
|
|
7
|
+
AST-only so the ruleset also runs on oxlint (which has no type-checker).
|
|
8
|
+
|
|
9
|
+
**Decision.** Keep `recommended` exactly as-is — AST-only, zero-FP, oxlint-
|
|
10
|
+
portable — and add a second config **`recommendedTypeChecked`** that turns on the
|
|
11
|
+
type-aware paths in one line, mirroring typescript-eslint's own
|
|
12
|
+
`recommended` / `recommendedTypeChecked` naming that users already know. The
|
|
13
|
+
per-rule `typescriptEnabled` option remains the underlying mechanism; the config
|
|
14
|
+
is just the discoverable door. The type-aware paths stay **purely additive**
|
|
15
|
+
(ADR-0002) — they find more, never flip a verdict on correct code — so there is
|
|
16
|
+
no corrective variance between the two configs.
|
|
17
|
+
|
|
18
|
+
## Consequences
|
|
19
|
+
|
|
20
|
+
- oxlint users and the speed-conscious stay on `recommended`; the majority who
|
|
21
|
+
run `tsc` + typescript-eslint flip to `recommendedTypeChecked` and get
|
|
22
|
+
cross-file component detection, nominal accessor detection, re-exported Solid API/control-flow
|
|
23
|
+
recognition, and sound `prefer-for` coverage without hunting per-rule flags.
|
|
24
|
+
- `prefer-for` is enabled only in `recommendedTypeChecked`: syntax alone cannot
|
|
25
|
+
distinguish Array#map from an unrelated collection method without false positives.
|
|
26
|
+
- The type-aware paths of `no-leaf-owner-operations`, `no-stale-props-alias`, and
|
|
27
|
+
`no-untracked-read-in-effect-apply` are enabled by the config as additive coverage.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Merge the forbidden-scope rules into one `no-leaf-owner-operations` rule
|
|
2
|
+
|
|
3
|
+
**Context.** `no-cleanup-in-forbidden-scope`, `no-flush-in-forbidden-scope`, and
|
|
4
|
+
`no-primitives-in-forbidden-scope` were three ~95%-identical files: same
|
|
5
|
+
leaf-owner detection (`createTrackedEffect`/`onSettled`), same enter/exit stack,
|
|
6
|
+
same binding check — differing only in the matched name-set and the message.
|
|
7
|
+
All three are facets of **one** runtime constraint: a leaf owner cannot own or
|
|
8
|
+
schedule. There is no sensible config that forbids `flush` in a leaf owner but
|
|
9
|
+
allows `onCleanup` — they are invalid for the identical reason. (ESLint
|
|
10
|
+
traverses each file once and fans out to all rules, so three rules bought no
|
|
11
|
+
extra cost to remove — this is about code shape and API surface, not perf.)
|
|
12
|
+
|
|
13
|
+
**Decision.** Merge the three into a single rule `no-leaf-owner-operations` with
|
|
14
|
+
one `messageId` per violation kind (so the distinct remediation messages
|
|
15
|
+
survive) and one doc page explaining leaf owners. Acceptable now because the
|
|
16
|
+
plugin is pre-adoption (ADR-0001), so the breaking rename of three rule IDs
|
|
17
|
+
costs effectively nothing. Apply the same treatment to the parallel pair
|
|
18
|
+
`no-signal-in-effect-apply` + `no-store-proxy-in-effect-apply`, merged into
|
|
19
|
+
`no-untracked-read-in-effect-apply` (both are "untracked read in the apply
|
|
20
|
+
phase" — one concept, one rule, with `signalRead`/`storeProxyRead` messageIds).
|
|
21
|
+
The two detection algorithms differ, so they are preserved intact inside the one
|
|
22
|
+
rule rather than rewritten; the merge unifies the rule ID, doc, and config entry.
|
|
23
|
+
|
|
24
|
+
## Consequences
|
|
25
|
+
|
|
26
|
+
- Three rule IDs disappear from `recommended` and any disable comments — a
|
|
27
|
+
breaking change, hence done before adoption rather than after.
|
|
28
|
+
- Do **not** re-split into per-operation rules: the granularity buys nothing
|
|
29
|
+
(the operations are never independently desirable) and reintroduces the
|
|
30
|
+
duplication this removes.
|
|
31
|
+
- **Correction (was inaccurate):** an earlier draft of this ADR claimed "the
|
|
32
|
+
shared leaf-owner/scope-stack machinery lives in `solid-rule-utils.ts`." It
|
|
33
|
+
does not. Solid binding resolution is now shared by `analysis/solid-bindings.ts`, while
|
|
34
|
+
computation callback classification is shared by `analysis/computation-roles.ts`. The
|
|
35
|
+
**scope-tracking state itself is per-rule and
|
|
36
|
+
intentionally different**: `no-leaf-owner-operations` keeps a `forbiddenStack`
|
|
37
|
+
(it must know nesting for any descendant call), while
|
|
38
|
+
`no-untracked-read-in-effect-apply` keeps an `applyCallbacks` set of
|
|
39
|
+
pre-registered callbacks (it only queries known apply functions). A shared
|
|
40
|
+
`reactiveScopeTracker` seam was considered (the two rules are two adapters, so
|
|
41
|
+
the seam is real) but deferred: the two query shapes differ enough that a
|
|
42
|
+
unifying interface risks leaking. Revisit only if a third scope-kind appears.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# The target audience runs TypeScript
|
|
2
|
+
|
|
3
|
+
**Context.** The "No value in a TypeScript codebase" deletion criterion
|
|
4
|
+
([ADR-0001](./0001-only-false-positive-free-rules.md)) removes a rule when `tsc`
|
|
5
|
+
already flags the same problem and the rule adds no autofix or migration
|
|
6
|
+
guidance. Six rules were deleted on that basis: `jsx-no-undef`, `jsx-uses-vars`,
|
|
7
|
+
`no-react-deps`, `no-invalid-cleanup-return`, `no-array-handlers`, and
|
|
8
|
+
`no-unknown-namespaces`. (Confirmed afresh for `no-invalid-cleanup-return`: tsc
|
|
9
|
+
rejects a non-function literal cleanup return under both plausible Solid typings
|
|
10
|
+
`void | CleanupFn` and `CleanupFn | undefined` — the void-return-bivariance
|
|
11
|
+
exception applies only to a _bare_ `void` target, never a union, so the rule was
|
|
12
|
+
genuinely redundant.)
|
|
13
|
+
|
|
14
|
+
That criterion silently assumes the user runs `tsc`. But the lean `recommended`
|
|
15
|
+
config is deliberately AST-only and oxlint-compatible, and AST-only/oxlint is
|
|
16
|
+
exactly the toolchain a plain-JavaScript Solid project would use — for which
|
|
17
|
+
`tsc` flags nothing and all six deleted rules would regain real, zero-FP value.
|
|
18
|
+
The assumption needed to be made explicit and chosen, not left implicit.
|
|
19
|
+
|
|
20
|
+
**Decision.** The target audience is **TypeScript Solid 2.0 codebases**. A
|
|
21
|
+
pure-JavaScript Solid project with no `tsc` in the loop is **out of scope**. The
|
|
22
|
+
AST-only nature of `recommended` is about ESLint/oxlint engine compatibility and
|
|
23
|
+
speed only — it does **not** imply we serve a JS-only audience, and it does not
|
|
24
|
+
re-open the six tsc-subsumed deletions.
|
|
25
|
+
|
|
26
|
+
## Consequences
|
|
27
|
+
|
|
28
|
+
- The six tsc-subsumed deletions stand on the stated assumption. Do not resurrect
|
|
29
|
+
them to serve JS users; that slice is explicitly unsupported.
|
|
30
|
+
- Solid 2.0 adoption is overwhelmingly TypeScript (the 2.0 reactivity/types story
|
|
31
|
+
assumes it), so this matches the real audience rather than narrowing it.
|
|
32
|
+
- If a JS audience ever becomes a goal, that is a deliberate scope expansion that
|
|
33
|
+
reopens this ADR and the six deletions together — not an incremental tweak.
|
|
34
|
+
- `typescript` stays a peer dependency (already required by the opt-in type-aware
|
|
35
|
+
paths, see [ADR-0002](./0002-sound-component-detection.md)).
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# `solid/components-return-once`
|
|
2
|
+
|
|
3
|
+
Keep component JSX returns in one stable place.
|
|
4
|
+
|
|
5
|
+
Solid components run once. If a component returns different JSX trees from separate `return` sites, or returns top-level JSX conditionals directly, and the decision is **reactive**, that control flow is no longer modeled inside Solid's reactive graph — it is evaluated a single time and goes stale. Keep the branching inside JSX with `<Show />`, `<Switch />`, fragments, or nested render helpers.
|
|
6
|
+
|
|
7
|
+
A return decided by a **static** guard is fine: `if (isServer) return <noscript />;` or `import.meta.env.DEV` can never change after mount, so a one-time decision is exactly right. The rule therefore reports a conditional or early return only when its guard provably performs a reactive read: a `props` member access, a locally-declared signal/memo accessor call, a store read, or a `const` derived from one of those. Guards it cannot prove reactive (a helper call, a context read, an accessor imported from another file) are not reported — a tolerated false negative; the runtime's `STRICT_READ_UNTRACKED` still covers executed paths.
|
|
8
|
+
|
|
9
|
+
## Bad
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
function Component(props) {
|
|
13
|
+
const [loading] = createSignal(true);
|
|
14
|
+
if (loading()) {
|
|
15
|
+
return <Spinner />;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return <Content />;
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
function Component(props) {
|
|
24
|
+
return props.ready ? <Content /> : <Fallback />;
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
function Component(props) {
|
|
30
|
+
return props.ready && <Content />;
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Good
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
function Component() {
|
|
38
|
+
return (
|
|
39
|
+
<Show when={loading()} fallback={<Content />}>
|
|
40
|
+
<Spinner />
|
|
41
|
+
</Show>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
function Component() {
|
|
48
|
+
return (
|
|
49
|
+
<Show when={ready()} fallback={<Fallback />}>
|
|
50
|
+
<Content />
|
|
51
|
+
</Show>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
function Component() {
|
|
58
|
+
const renderBody = () => {
|
|
59
|
+
if (ready()) return <Content />;
|
|
60
|
+
return <Fallback />;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return <section>{renderBody()}</section>;
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
// Static guards are allowed — the decision cannot change after the single run.
|
|
69
|
+
function Page() {
|
|
70
|
+
if (isServer) return <noscript />;
|
|
71
|
+
return <div />;
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Notes
|
|
76
|
+
|
|
77
|
+
- Early component returns are reported (when reactively guarded), but not autofixed.
|
|
78
|
+
- Final ternary returns can often be autofixed to `<Show />` or `<Switch />`.
|
|
79
|
+
- Final `&&` returns are reported but not autofixed because `0 && <X />` and `<Show when={0}>` are not equivalent.
|
|
80
|
+
|
|
81
|
+
## Options
|
|
82
|
+
|
|
83
|
+
### `typescriptEnabled` (default `false`)
|
|
84
|
+
|
|
85
|
+
By default a function is recognised as a component only when it is annotated `Component`/`VoidComponent`/`ParentComponent`/`FlowComponent` or used as `<C/>` in the same file — so a bare, unannotated component used only in another file is not checked. Set `typescriptEnabled: true` to also detect components by their cross-file JSX usage via the TypeScript type-checker. This requires ESLint type information and is slower.
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{ "rules": { "solid/components-return-once": ["warn", { "typescriptEnabled": true }] } }
|
|
89
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Solid 2 dev-diagnostic coverage
|
|
2
|
+
|
|
3
|
+
The plugin complements Solid's
|
|
4
|
+
[dev-mode diagnostics](https://github.com/solidjs/solid/blob/next/documentation/solid-2.0/08-dev-diagnostics.md).
|
|
5
|
+
It reports a diagnostic statically only when the source proves the invalid runtime context. Dynamic
|
|
6
|
+
ownership, pending state, disposal state, and scheduler behavior remain with the runtime.
|
|
7
|
+
|
|
8
|
+
| Solid diagnostic | Static coverage | Rule or owner |
|
|
9
|
+
| -------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- |
|
|
10
|
+
| `REACTIVE_WRITE_IN_OWNED_SCOPE` | Strong, sound subset | `no-owned-scope-writes` |
|
|
11
|
+
| `PENDING_ASYNC_UNTRACKED_READ` | Partial; catches provable untracked reads, regardless of pending state | `no-stale-props-alias`, `no-destructure`, `no-untracked-read-in-effect-apply` |
|
|
12
|
+
| `ASYNC_OUTSIDE_LOADING_BOUNDARY` | Runtime only; loading ancestry and render timing are dynamic | Solid runtime |
|
|
13
|
+
| `CLEANUP_IN_FORBIDDEN_SCOPE` | Strong | `no-leaf-owner-operations` |
|
|
14
|
+
| `SETTLED_CLEANUP_UNOWNED` | Runtime only; ownership at execution determines whether cleanup is honored | Solid runtime |
|
|
15
|
+
| `PRIMITIVE_IN_FORBIDDEN_SCOPE` | Strong for owner-creating primitives; plain value signals/stores follow current runtime behavior | `no-leaf-owner-operations` |
|
|
16
|
+
| Invalid cleanup return | Type-level cases are intentionally delegated | TypeScript and Solid runtime |
|
|
17
|
+
| `flush()` in forbidden scope | Strong | `no-leaf-owner-operations` |
|
|
18
|
+
| Potential infinite loop | Runtime only; depends on executed graph behavior | Solid runtime |
|
|
19
|
+
| `STRICT_READ_UNTRACKED` | Strong, sound subset of binding-proven reads | `no-stale-props-alias`, `no-destructure`, `no-untracked-read-in-effect-apply` |
|
|
20
|
+
| `PENDING_ASYNC_FORBIDDEN_SCOPE` | Runtime only; the source must actually be pending when read | Solid runtime |
|
|
21
|
+
| `NO_OWNER_EFFECT` | Runtime only; the active owner is execution-context state | Solid runtime |
|
|
22
|
+
| `NO_OWNER_CLEANUP` | Runtime only; the active owner is execution-context state | Solid runtime |
|
|
23
|
+
| `NO_OWNER_BOUNDARY` | Runtime only; the active owner is execution-context state | Solid runtime |
|
|
24
|
+
| `RUN_WITH_DISPOSED_OWNER` | Runtime only; disposal is dynamic | Solid runtime |
|
|
25
|
+
|
|
26
|
+
Some plugin rules have no corresponding runtime diagnostic. For example,
|
|
27
|
+
`no-reactive-read-after-await` catches dependency loss that native async continuation semantics make
|
|
28
|
+
indistinguishable from an intentional untracked read at runtime. The control-flow and JSX rules
|
|
29
|
+
likewise protect static structure or style rather than mirroring a dev diagnostic.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# `solid/jsx-no-duplicate-props`
|
|
2
|
+
|
|
3
|
+
Disallow competing JSX content sources.
|
|
4
|
+
|
|
5
|
+
TypeScript already reports repeated JSX attributes, so this rule focuses only on Solid/DOM content
|
|
6
|
+
semantics that the type system does not express: `children`, meaningful JSX children, `innerHTML`,
|
|
7
|
+
and `textContent` cannot define the same host element at the same time.
|
|
8
|
+
|
|
9
|
+
## Bad
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
<div children={<span />}>
|
|
13
|
+
<span />
|
|
14
|
+
</div>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<div innerHTML="<p>Hello</p>" textContent="Hello" />
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Good
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
<div>
|
|
25
|
+
<span />
|
|
26
|
+
</div>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
<div innerHTML={html}>{/* comments and formatting whitespace are not content */}</div>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
On custom components these names are author-defined props, so the host-element conflict checks do
|
|
34
|
+
not apply.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# `solid/no-destructure`
|
|
2
|
+
|
|
3
|
+
Disallow destructuring component props.
|
|
4
|
+
|
|
5
|
+
In Solid 2, destructuring props performs top-level untracked reads. That breaks reactivity in the same way as assigning `const title = props.title` in a component body.
|
|
6
|
+
|
|
7
|
+
## Bad
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
function Component({ title }) {
|
|
11
|
+
return <h1>{title}</h1>;
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
function Component({ title: heading }) {
|
|
17
|
+
return <h1>{heading}</h1>;
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
function Component({ title = "Untitled" }) {
|
|
23
|
+
return <h1>{title}</h1>;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
function Component({ title, ...rest }) {
|
|
29
|
+
return <Card {...rest}>{title}</Card>;
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// destructuring the props object in the body is the same untracked read
|
|
35
|
+
function Component(props) {
|
|
36
|
+
const { title } = props;
|
|
37
|
+
return <h1>{title}</h1>;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Good
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
function Component(props) {
|
|
45
|
+
return <h1>{props.title}</h1>;
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
function Component(_props) {
|
|
51
|
+
const props = merge({ title: "Untitled" }, _props);
|
|
52
|
+
return <h1>{props.title}</h1>;
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
function Component(props) {
|
|
58
|
+
const rest = omit(props, "title");
|
|
59
|
+
return <Card {...rest}>{props.title}</Card>;
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
function Component(props: Props) {
|
|
65
|
+
return <div p1={props.prop1} p2={props.prop2} />;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Notes
|
|
70
|
+
|
|
71
|
+
- Simple destructures autofix directly to `props.foo` reads.
|
|
72
|
+
- Helper-based fixes may rewrite to `merge(...)` and `omit(...)`; the fix also adds the
|
|
73
|
+
`solid-js` import for them when it is missing, so the fixed code always compiles.
|
|
74
|
+
- If `merge` or `omit` is already used for something else in the file, the rule stays report-only for that case.
|
|
75
|
+
- **The autofix changes observable behavior — deliberately.** The flagged code freezes prop values
|
|
76
|
+
at setup (the bug); the rewrite makes every read live. If a value was _intentionally_ frozen,
|
|
77
|
+
wrap it in `untrack(() => props.x)` instead of destructuring — that form is not reported. This is
|
|
78
|
+
a knowing exception to the "safe autofixes cannot change behavior" tier: the tier exists to
|
|
79
|
+
protect _correct_ code from corruption, and code flagged by this rule is incorrect by the
|
|
80
|
+
runtime's own `STRICT_READ_UNTRACKED` contract.
|
|
81
|
+
- Arrow-body helper cases like `({ a = 5 }) => <div a={a} />` are reported but not autofixed.
|
|
82
|
+
- Destructuring the props object in the body (`const { a } = props`) is also reported (report-only, no autofix). Only destructures of the component's first parameter are flagged — destructuring an arbitrary object is fine.
|
|
83
|
+
|
|
84
|
+
## Options
|
|
85
|
+
|
|
86
|
+
### `typescriptEnabled` (default `false`)
|
|
87
|
+
|
|
88
|
+
A function is recognised as a component by annotation (`Component`/…) or in-file `<C/>` usage by default. Set `typescriptEnabled: true` to also detect components by their cross-file JSX usage via the TypeScript type-checker (slower; requires ESLint type information).
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{ "rules": { "solid/no-destructure": ["warn", { "typescriptEnabled": true }] } }
|
|
92
|
+
```
|