From 2461a43b1d1b4e2d14cd2fd2d2239f978365ded1 Mon Sep 17 00:00:00 2001 From: oekazuma Date: Sun, 9 Aug 2026 01:53:07 +0900 Subject: [PATCH 1/3] fix(core): stop effect-as-onmount flagging member-read reactivity; align effect-rule docs --- .changeset/effect-rules-validity.md | 23 ++++++++++ .../ja/rules/correctness/effect-as-derived.md | 26 ++++++++++- .../ja/rules/correctness/effect-as-onmount.md | 13 ++++-- .../docs/ja/rules/correctness/index.mdx | 2 +- .../correctness/instance-browser-global.md | 18 +++++++- .../correctness/server-browser-global.md | 8 ++-- docs/src/content/docs/ja/rules/index.mdx | 2 +- .../rules/correctness/effect-as-derived.md | 28 +++++++++++- .../rules/correctness/effect-as-onmount.md | 13 ++++-- .../content/docs/rules/correctness/index.mdx | 2 +- .../correctness/instance-browser-global.md | 18 +++++++- .../correctness/server-browser-global.md | 8 ++-- docs/src/content/docs/rules/index.mdx | 2 +- ...-02-correct003-effect-as-onmount-design.md | 16 +++++++ packages/core/src/component-parse.ts | 44 ++++++++++++++++++- .../rules/correctness/effect-as-onmount.ts | 10 +++-- packages/core/test/component-parse.test.ts | 32 ++++++++++++++ packages/core/test/correctness-rules.test.ts | 10 +++++ 18 files changed, 247 insertions(+), 28 deletions(-) create mode 100644 .changeset/effect-rules-validity.md diff --git a/.changeset/effect-rules-validity.md b/.changeset/effect-rules-validity.md new file mode 100644 index 000000000..b5ed5faf9 --- /dev/null +++ b/.changeset/effect-rules-validity.md @@ -0,0 +1,23 @@ +--- +'@svelte-vitals/core': patch +--- + +Two `correctness/effect-*` rule content fixes from the v1.0 rule-validity review: + +- `correctness/effect-as-onmount` no longer flags an `$effect` that reads a reactive value through + a member expression on an imported binding or a `new`-ed local — a class instance with `$state` + fields, a `SvelteMap`/`SvelteSet`, an imported runes-module state object, and + `svelte/reactivity/window` were all indistinguishable from the true mount-only positive, and the + rule's "use `onMount` instead" advice would have converted a re-running effect into run-once. The + change is strictly narrowing (only removes findings, never adds one); the rule's message, + recommendation, and docs now name `{@attach}` and event handlers alongside `onMount` instead of + presenting `onMount` as the sole fix, and the docs admit the one remaining blind spot (a reactive + value reached only through a plain function's return value). +- The documented fix snippets for `correctness/server-browser-global` and + `correctness/instance-browser-global` now use `onMount` (and, for the latter, + `svelte/reactivity/window` as the preferred modern form) instead of an `$effect` that assigns a + browser global to `$state` — that snippet was itself flagged by `correctness/effect-as-derived`, + whose "use `$derived`" advice would reintroduce the SSR `ReferenceError` those two rules exist to + prevent. `correctness/effect-as-derived`'s docs gain this as a second known limitation, and drop + an inaccurate "$derived updates synchronously" claim in favor of describing its actual push-pull, + lazy-recompute behavior. diff --git a/docs/src/content/docs/ja/rules/correctness/effect-as-derived.md b/docs/src/content/docs/ja/rules/correctness/effect-as-derived.md index b5a0c39a4..4aa55ca78 100644 --- a/docs/src/content/docs/ja/rules/correctness/effect-as-derived.md +++ b/docs/src/content/docs/ja/rules/correctness/effect-as-derived.md @@ -11,7 +11,7 @@ description: 状態を代入するだけの $effect は $derived に置き換え ## なぜ重要か -`$effect` で状態を同期する書き方(React の useEffect の習慣の持ち込み)は、レンダリング後に実行されるため、余計なレンダリングパスやループを招きます。`$derived` は同じ依存関係を宣言的に表し、同期的に更新されます。 +`$effect` で状態を同期する書き方(React の useEffect の習慣の持ち込み)は、レンダリング後に実行されるため、余計なレンダリングパスやループを招きます。`$derived` は同じ依存関係を宣言的に表します — 個別の effect 実行をスケジュールするのではなく、次に読み取られたタイミングで遅延評価されます。 ## 修正方法 @@ -40,4 +40,26 @@ description: 状態を代入するだけの $effect は $derived に置き換え ``` -`$derived` は hydration 中も含めて即座に評価されますが、`$effect` は mount の1ティック後に実行されます。それがこのパターンの狙いです。この形を `$derived` に置き換えるのは `$effect` が防いでいたバグの再発なので、「修正」せず [`svelte-vitals-disable-next-line`](/ja/guides/cli#特定の指摘だけをインラインで抑制する) コメントで抑制してください。 +`$derived` は hydration 中も含めて次に読まれたタイミングで再計算されますが、`$effect` は mount の1ティック後に実行されます。それがこのパターンの狙いです。この形を `$derived` に置き換えるのは `$effect` が防いでいたバグの再発なので、「修正」せず [`svelte-vitals-disable-next-line`](/ja/guides/cli#特定の指摘だけをインラインで抑制する) コメントで抑制してください。 + +## 既知の制限: browser global の捕捉 + +同じ構造的な死角は、[correctness/server-browser-global](/ja/rules/correctness/server-browser-global) と +[correctness/instance-browser-global](/ja/rules/correctness/instance-browser-global) が示している修正例 +—— browser 専用の global を読んで `$state` に代入する `$effect` —— も検出してしまいます: + +```svelte + +``` + +これを `$derived(localStorage.getItem('filters'))` に置き換えると、その式は SSR 中にも評価されてしまい、 +この2つのルールが防ごうとしている `ReferenceError: localStorage is not defined` を再発させます。 +`localStorage`/`window` などは、まさに `$derived` が安全に読めない値です —— derived の式はサーバーサイド +レンダリング中にも実行され得るからです。`onMount`、あるいは `window` のプロパティなら +[`svelte/reactivity/window`](https://svelte.dev/docs/svelte/svelte-reactivity-window) を使ってください +(修正方法はこの2つのルールのドキュメントを参照)。`$derived` に切り替えるのではなく、この指摘は抑制してください。 diff --git a/docs/src/content/docs/ja/rules/correctness/effect-as-onmount.md b/docs/src/content/docs/ja/rules/correctness/effect-as-onmount.md index c7c90b64b..a5e49a42d 100644 --- a/docs/src/content/docs/ja/rules/correctness/effect-as-onmount.md +++ b/docs/src/content/docs/ja/rules/correctness/effect-as-onmount.md @@ -1,17 +1,17 @@ --- title: correctness/effect-as-onmount · onMount 代わりの $effect -description: リアクティブな値を読まない $effect は、onMount で十分です。 +description: reactive 値を読まない $effect は、イベントハンドラ、{@attach}、onMount のいずれかで書くべきです。 --- **重大度:** warning · **カテゴリ:** correctness ## チェック内容 -本体が空ではないのに reactive 値を一切読まない `$effect` / `$effect.pre` を検出します。`$state`、`$derived`、`$props` の参照、store 購読、裸の関数呼び出し(`foo()`)のいずれもないものです。そのような effect はマウント後に一度だけ実行され、再実行されません。コンポーネントのインスタンススクリプトを静的(CLI)解析します。 +本体が空ではないのに、このチェックから見える reactive 値を一切読まない `$effect` / `$effect.pre` を検出します。`$state`、`$derived`、`$props`、import されたバインディング、`new` で初期化されたローカル変数、store 購読のいずれもなく、裸の関数呼び出し(`foo()`)もないものです。そのような effect はマウント後に一度だけ実行され、このチェックが追跡できる範囲では再実行されません。コンポーネントのインスタンススクリプトを静的(CLI)解析します。 ## なぜ重要か -何にも反応しない `$effect` は実質 `onMount` です。`$effect` で書くとその意図が読み取りにくくなるうえ、リアクティビティの仕組みの誤用でもあります。`onMount` なら「マウント時に一度だけ実行する」という意図をそのまま表現できます。 +何にも反応しない `$effect` は、多くの場合イベントハンドラ、`{@attach}`、`onMount` の代わりとして遠回りなだけです。`$effect` で書くとその意図が読み取りにくくなるうえ、リアクティビティの仕組みの誤用でもあります。 ## 修正方法 @@ -24,3 +24,10 @@ description: リアクティブな値を読まない $effect は、onMount で }); ``` + +ユーザー操作への応答として実行しているなら、`$effect` よりイベントハンドラを優先してください。外部ライブラリ(チャートやツールチップなど)と要素を同期しているなら、[`{@attach ...}`](https://svelte.dev/docs/svelte/@attach) を優先してください — `$effect` や `onMount` より現在推奨されている DOM/ライブラリ同期の方法です。 + +## 既知の制限: 素の関数呼び出しの戻り値経由のリアクティビティ + +このチェックは、rune の宣言(`$state`/`$derived`/`$props`)、import されたバインディング、`new …()` で初期化されたローカル変数など、たどれる名前を経由した reactive 読み取りを認識します — これはクラスの `$state` フィールド、`SvelteMap`/`SvelteSet`、import された runes モジュールの状態オブジェクト、`svelte/reactivity/window` をカバーします。素の関数の戻り値(`const c = createCounter()`)経由でしか到達できない reactive な値には、たどれる名前がないため、そのように書かれた本物の reactive な effect でも検出されてしまうことがあります。該当する場合は、動作しているコードを `onMount` に移すのではなく、 +[`svelte-vitals-disable-next-line`](/ja/guides/cli#特定の指摘だけをインラインで抑制する) コメントで抑制してください。 diff --git a/docs/src/content/docs/ja/rules/correctness/index.mdx b/docs/src/content/docs/ja/rules/correctness/index.mdx index 56dd0a7ba..fc2e897dd 100644 --- a/docs/src/content/docs/ja/rules/correctness/index.mdx +++ b/docs/src/content/docs/ja/rules/correctness/index.mdx @@ -17,7 +17,7 @@ description: svelte-vitals の Correctness ルール一覧。 | [`correctness/each-index-key`](/ja/rules/correctness/each-index-key) | 🟡 warning | {#each} のキーに index を使うと、要素の同一性が位置で決まります。キーを付けないのと同じバグが、見えにくい形で起こります。 | | [`correctness/each-key`](/ja/rules/correctness/each-key) | 🟡 warning | 動的なデータを回す {#each} にはキーを付けましょう。 | | [`correctness/effect-as-derived`](/ja/rules/correctness/effect-as-derived) | 🟡 warning | 状態を代入するだけの $effect は $derived に置き換えましょう。 | -| [`correctness/effect-as-onmount`](/ja/rules/correctness/effect-as-onmount) | 🟡 warning | リアクティブな値を読まない $effect は、onMount で十分です。 | +| [`correctness/effect-as-onmount`](/ja/rules/correctness/effect-as-onmount) | 🟡 warning | reactive 値を読まない $effect は、イベントハンドラ、{@attach}、onMount のいずれかで書くべきです。 | | [`correctness/instance-browser-global`](/ja/rules/correctness/instance-browser-global) | 🟡 warning | インスタンススクリプトは SSR 時にサーバーでも実行されるため、トップレベルで window や document を参照するとレンダリングが落ちます。 | | [`correctness/nonreactive-builtin-state`](/ja/rules/correctness/nonreactive-builtin-state) | 🟡 warning | $state に入れた素の Map・Set・Date・URL・URLSearchParams はプロキシされないため、中身を変えても検知されず、気づかないうちに UI が更新されなくなります。 | | [`correctness/prop-mutation`](/ja/rules/correctness/prop-mutation) | 🟡 warning | $bindable を宣言していない $props() の値は書き換えないでください。 | diff --git a/docs/src/content/docs/ja/rules/correctness/instance-browser-global.md b/docs/src/content/docs/ja/rules/correctness/instance-browser-global.md index d6a2310d8..1b0bea23a 100644 --- a/docs/src/content/docs/ja/rules/correctness/instance-browser-global.md +++ b/docs/src/content/docs/ja/rules/correctness/instance-browser-global.md @@ -19,13 +19,27 @@ description: インスタンススクリプトは SSR 時にサーバーでも ## 修正方法 +`window` のプロパティなら、[`svelte/reactivity/window`](https://svelte.dev/docs/svelte/svelte-reactivity-window)(5.11.0 以降)が現在推奨される形です — ガード不要で、サーバーでは `undefined`、クライアントではリアクティブになります: + ```svelte + +

{innerWidth.current}

+``` + +`svelte/reactivity/window` がカバーしない場合は、`onMount` の中で読んでください — `onMount` はサーバーでは実行されません: + +```svelte + ``` diff --git a/docs/src/content/docs/ja/rules/correctness/server-browser-global.md b/docs/src/content/docs/ja/rules/correctness/server-browser-global.md index 4c1e13558..e524aa7a1 100644 --- a/docs/src/content/docs/ja/rules/correctness/server-browser-global.md +++ b/docs/src/content/docs/ja/rules/correctness/server-browser-global.md @@ -36,14 +36,16 @@ export function load() { } ``` -ブラウザアクセスをクライアント側へ移します: +ブラウザアクセスをクライアント側、`onMount` の中へ移します — `onMount` はサーバーでは実行されません: ```svelte ``` diff --git a/docs/src/content/docs/ja/rules/index.mdx b/docs/src/content/docs/ja/rules/index.mdx index 83fe45ee8..0414af436 100644 --- a/docs/src/content/docs/ja/rules/index.mdx +++ b/docs/src/content/docs/ja/rules/index.mdx @@ -95,7 +95,7 @@ svelte-vitals が報告するルールをカテゴリ別にまとめました。 | [`correctness/each-index-key`](/ja/rules/correctness/each-index-key) | 🟡 warning | {#each} のキーに index を使うと、要素の同一性が位置で決まります。キーを付けないのと同じバグが、見えにくい形で起こります。 | | [`correctness/each-key`](/ja/rules/correctness/each-key) | 🟡 warning | 動的なデータを回す {#each} にはキーを付けましょう。 | | [`correctness/effect-as-derived`](/ja/rules/correctness/effect-as-derived) | 🟡 warning | 状態を代入するだけの $effect は $derived に置き換えましょう。 | -| [`correctness/effect-as-onmount`](/ja/rules/correctness/effect-as-onmount) | 🟡 warning | リアクティブな値を読まない $effect は、onMount で十分です。 | +| [`correctness/effect-as-onmount`](/ja/rules/correctness/effect-as-onmount) | 🟡 warning | reactive 値を読まない $effect は、イベントハンドラ、{@attach}、onMount のいずれかで書くべきです。 | | [`correctness/instance-browser-global`](/ja/rules/correctness/instance-browser-global) | 🟡 warning | インスタンススクリプトは SSR 時にサーバーでも実行されるため、トップレベルで window や document を参照するとレンダリングが落ちます。 | | [`correctness/nonreactive-builtin-state`](/ja/rules/correctness/nonreactive-builtin-state) | 🟡 warning | $state に入れた素の Map・Set・Date・URL・URLSearchParams はプロキシされないため、中身を変えても検知されず、気づかないうちに UI が更新されなくなります。 | | [`correctness/prop-mutation`](/ja/rules/correctness/prop-mutation) | 🟡 warning | $bindable を宣言していない $props() の値は書き換えないでください。 | diff --git a/docs/src/content/docs/rules/correctness/effect-as-derived.md b/docs/src/content/docs/rules/correctness/effect-as-derived.md index 27f53518d..7d87f1cad 100644 --- a/docs/src/content/docs/rules/correctness/effect-as-derived.md +++ b/docs/src/content/docs/rules/correctness/effect-as-derived.md @@ -11,7 +11,7 @@ Flags an `$effect` whose body only assigns to `$state` variables. Checked by sta ## Why it matters -Synchronising state with an `$effect` (the "useEffect → $effect" habit from React) runs after render and can trigger extra render passes or loops. A `$derived` value expresses the same dependency declaratively and updates synchronously. +Synchronising state with an `$effect` (the "useEffect → $effect" habit from React) runs after render and can trigger extra render passes or loops. A `$derived` value expresses the same dependency declaratively — Svelte recomputes it lazily, the next time it's read, rather than scheduling a separate effect run. ## How to fix @@ -42,8 +42,32 @@ the "mount signal" idiom used to avoid SSR/prerender ↔ hydration mismatches: ``` -`$derived` evaluates eagerly, including during hydration; `$effect` runs one tick after mount, +`$derived` recomputes on next read, including during hydration; `$effect` runs one tick after mount, which is the whole point here. Converting this shape to `$derived` reintroduces the bug the `$effect` was added to prevent — so suppress it with a [`svelte-vitals-disable-next-line`](/guides/cli#suppressing-a-single-finding-inline) comment rather than "fixing" it. + +## Known limitation: browser-global capture + +The same structural blind spot also catches the documented fix for +[correctness/server-browser-global](/rules/correctness/server-browser-global) and +[correctness/instance-browser-global](/rules/correctness/instance-browser-global) — an `$effect` that +reads a browser-only global and assigns it to `$state`: + +```svelte + +``` + +Converting this to `$derived(localStorage.getItem('filters'))` would evaluate the expression during +SSR — reintroducing the exact `ReferenceError: localStorage is not defined` those two rules exist to +prevent. `localStorage`/`window`/etc. are exactly the values `$derived` cannot safely read, since a +derived expression can run during server-side rendering. Prefer `onMount`, or +[`svelte/reactivity/window`](https://svelte.dev/docs/svelte/svelte-reactivity-window) for `window` +properties — see those two rules' docs for the fix — and suppress this finding rather than switching to +`$derived`. diff --git a/docs/src/content/docs/rules/correctness/effect-as-onmount.md b/docs/src/content/docs/rules/correctness/effect-as-onmount.md index bbb86bd1e..fe70e3732 100644 --- a/docs/src/content/docs/rules/correctness/effect-as-onmount.md +++ b/docs/src/content/docs/rules/correctness/effect-as-onmount.md @@ -1,17 +1,17 @@ --- title: correctness/effect-as-onmount · Effect used as onMount -description: Use onMount for an $effect that reads no reactive value. +description: An $effect that reads no reactive value belongs in an event handler, {@attach}, or onMount instead. --- **Severity:** warning · **Category:** correctness ## What it checks -Flags an `$effect` / `$effect.pre` whose non-empty body reads no reactive value — no `$state`, `$derived`, or `$props`, no store subscription, and no bare function call (`foo()`). Such an effect runs once after mount and never re-runs. Checked by static (CLI) analysis of component instance scripts. +Flags an `$effect` / `$effect.pre` whose non-empty body reads no reactive value that this analysis can see — no `$state`, `$derived`, `$props`, imported binding, `new`-ed local, or store subscription, and no bare function call (`foo()`). Such an effect runs once after mount and never re-runs on the paths this check can follow. Checked by static (CLI) analysis of component instance scripts. ## Why it matters -An `$effect` that never reacts to anything is an `onMount` in disguise. Using `$effect` obscures that intent and misuses the reactivity system; `onMount` says "run this once when the component mounts" directly. +An `$effect` that never reacts to anything is usually a slower, less direct stand-in for an event handler, `{@attach}`, or `onMount`. Using `$effect` for it obscures intent and misuses the reactivity system. ## How to fix @@ -24,3 +24,10 @@ An `$effect` that never reacts to anything is an `onMount` in disguise. Using `$ }); ``` + +If the effect runs in response to a user interaction, prefer an event handler over `$effect`. If it syncs an element with an external library (e.g. a chart or a tooltip), prefer [`{@attach ...}`](https://svelte.dev/docs/svelte/@attach) instead — it's the current recommended way to do DOM/library sync, ahead of both `$effect` and `onMount`. + +## Known limitation: reactivity reached through a plain function call + +This check recognizes a reactive read through a name it can trace back to a rune declarator (`$state`/`$derived`/`$props`), an imported binding, or a local initialized with `new …()` — which covers class `$state` fields, `SvelteMap`/`SvelteSet`, an imported runes-module state object, and `svelte/reactivity/window`. A reactive value reached only through a plain function's return value (`const c = createCounter()`) has no such name to trace, so a genuinely reactive effect built that way can still be flagged. If that's your case, suppress it with a +[`svelte-vitals-disable-next-line`](/guides/cli#suppressing-a-single-finding-inline) comment rather than moving working code into `onMount`. diff --git a/docs/src/content/docs/rules/correctness/index.mdx b/docs/src/content/docs/rules/correctness/index.mdx index 012f3fbec..20f4200f7 100644 --- a/docs/src/content/docs/rules/correctness/index.mdx +++ b/docs/src/content/docs/rules/correctness/index.mdx @@ -17,7 +17,7 @@ Svelte 5 runes and lifecycle misuse — code that compiles but behaves wrong. | [`correctness/each-index-key`](/rules/correctness/each-index-key) | 🟡 warning | Keying an {#each} block by its index gives items position-based identity — the same bug as no key, masked. | | [`correctness/each-key`](/rules/correctness/each-key) | 🟡 warning | An {#each} block over dynamic data should have a key. | | [`correctness/effect-as-derived`](/rules/correctness/effect-as-derived) | 🟡 warning | Use $derived instead of an $effect that only assigns state. | -| [`correctness/effect-as-onmount`](/rules/correctness/effect-as-onmount) | 🟡 warning | Use onMount for an $effect that reads no reactive value. | +| [`correctness/effect-as-onmount`](/rules/correctness/effect-as-onmount) | 🟡 warning | An $effect that reads no reactive value belongs in an event handler, {@attach}, or onMount instead. | | [`correctness/instance-browser-global`](/rules/correctness/instance-browser-global) | 🟡 warning | A component's instance script runs on the server during SSR — window/document reads at its top level crash the render. | | [`correctness/nonreactive-builtin-state`](/rules/correctness/nonreactive-builtin-state) | 🟡 warning | A plain Map, Set, Date, URL, or URLSearchParams in $state is not proxied — its mutations are invisible to reactivity, and the UI silently stops updating. | | [`correctness/prop-mutation`](/rules/correctness/prop-mutation) | 🟡 warning | Don't mutate a prop from $props() unless it is declared $bindable. | diff --git a/docs/src/content/docs/rules/correctness/instance-browser-global.md b/docs/src/content/docs/rules/correctness/instance-browser-global.md index 6340c91c3..621d41fa3 100644 --- a/docs/src/content/docs/rules/correctness/instance-browser-global.md +++ b/docs/src/content/docs/rules/correctness/instance-browser-global.md @@ -19,13 +19,27 @@ A browser global used only as a `$props()` destructuring default (`let { width = ## How to fix +The preferred modern form for a `window` property is [`svelte/reactivity/window`](https://svelte.dev/docs/svelte/svelte-reactivity-window) (5.11.0+) — no guard needed, `undefined` on the server, reactive on the client: + ```svelte + +

{innerWidth.current}

+``` + +For anything `svelte/reactivity/window` doesn't cover, read the global in `onMount` — it never runs on the server: + +```svelte + ``` diff --git a/docs/src/content/docs/rules/correctness/server-browser-global.md b/docs/src/content/docs/rules/correctness/server-browser-global.md index adc091bfb..7b5c5819f 100644 --- a/docs/src/content/docs/rules/correctness/server-browser-global.md +++ b/docs/src/content/docs/rules/correctness/server-browser-global.md @@ -36,14 +36,16 @@ export function load() { } ``` -Move the browser access to the client side: +Move the browser access to the client side, in `onMount` — it never runs on the server: ```svelte ``` diff --git a/docs/src/content/docs/rules/index.mdx b/docs/src/content/docs/rules/index.mdx index 4dd9c69e9..d4e6fcf61 100644 --- a/docs/src/content/docs/rules/index.mdx +++ b/docs/src/content/docs/rules/index.mdx @@ -95,7 +95,7 @@ The severities below are the defaults — see [Configuration](/guides/configurat | [`correctness/each-index-key`](/rules/correctness/each-index-key) | 🟡 warning | Keying an {#each} block by its index gives items position-based identity — the same bug as no key, masked. | | [`correctness/each-key`](/rules/correctness/each-key) | 🟡 warning | An {#each} block over dynamic data should have a key. | | [`correctness/effect-as-derived`](/rules/correctness/effect-as-derived) | 🟡 warning | Use $derived instead of an $effect that only assigns state. | -| [`correctness/effect-as-onmount`](/rules/correctness/effect-as-onmount) | 🟡 warning | Use onMount for an $effect that reads no reactive value. | +| [`correctness/effect-as-onmount`](/rules/correctness/effect-as-onmount) | 🟡 warning | An $effect that reads no reactive value belongs in an event handler, {@attach}, or onMount instead. | | [`correctness/instance-browser-global`](/rules/correctness/instance-browser-global) | 🟡 warning | A component's instance script runs on the server during SSR — window/document reads at its top level crash the render. | | [`correctness/nonreactive-builtin-state`](/rules/correctness/nonreactive-builtin-state) | 🟡 warning | A plain Map, Set, Date, URL, or URLSearchParams in $state is not proxied — its mutations are invisible to reactivity, and the UI silently stops updating. | | [`correctness/prop-mutation`](/rules/correctness/prop-mutation) | 🟡 warning | Don't mutate a prop from $props() unless it is declared $bindable. | diff --git a/docs/superpowers/specs/2026-07-02-correct003-effect-as-onmount-design.md b/docs/superpowers/specs/2026-07-02-correct003-effect-as-onmount-design.md index f26d6d248..6b9b2d3c4 100644 --- a/docs/superpowers/specs/2026-07-02-correct003-effect-as-onmount-design.md +++ b/docs/superpowers/specs/2026-07-02-correct003-effect-as-onmount-design.md @@ -158,3 +158,19 @@ PERF010; the rule is CLI/static-only and no-ops in rendered mode, so not suppressed instead (conservative). - Reactive sources beyond `$state`/`$derived`/`$props`/stores. - Merging with CORRECT002's derive-smell detection (distinct signal). + +## 2026-08-09 addendum + +The "conservative — no false positives" claim in §1 was refuted by the 2026-08-09 v1.0 +rule-validity review (`docs/superpowers/specs/2026-08-09-v1-rule-validity-review.md`, +Priority 1 #1): `reactiveNames` only ever held same-file rune declarators, so a member read on +a class instance (`new Counter()`, `$state` fields), a `SvelteMap`/`SvelteSet`, an imported +runes-module state object, or `svelte/reactivity/window` was indistinguishable from the true +positive — all four yielded `mountOnly: true`, and the rule's advice to switch to `onMount` +would have frozen working reactive code. Fixed by narrowing detection: `bodyReadsReactive` now +also treats a member read on any imported binding or any local initialized with `new …()` as +reactive (`collectImportedLocalNames`/`collectNewExprLocalNames` in `component-parse.ts`), +folded into the same `reactiveNames` set. This is strictly narrower — it only ever suppresses a +finding, never adds one — and does not close the remaining gap: a reactive value reached only +through a plain function's return value still has no traceable name and can still be flagged +(documented as a known limitation in the rule's docs page instead of silently claimed away). diff --git a/packages/core/src/component-parse.ts b/packages/core/src/component-parse.ts index 56eff12f9..f28697427 100644 --- a/packages/core/src/component-parse.ts +++ b/packages/core/src/component-parse.ts @@ -921,9 +921,45 @@ function collectDirectiveEscapes(node: Node, names: Set, acc: Set