Repository navigation
feat(core): add CORRECT005 — flag mutation of a non-bindable $props #139
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| '@svelte-vitals/core': minor | ||
| 'svelte-vitals': minor | ||
| --- | ||
|
|
||
| Add CORRECT005: flag mutation of a non-`$bindable` prop destructured from `$props()` (member writes, `delete`, or a mutating method call like `.push()`). Plain reassignment of the prop itself is not flagged — Svelte's docs explicitly sanction that pattern for ephemeral state; only mutation is prohibited. Catches a class of bug the compiler never reports: mutating a plain-object prop is a silent no-op, and mutating a reactive-state-proxy prop only warns at runtime if that code path is exercised. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| --- | ||
| title: CORRECT005 · 非 bindable prop の変異 | ||
| description: $bindable を宣言していない $props() の値を変異させてはいけません。 | ||
| --- | ||
|
|
||
| **重大度:** warning · **カテゴリ:** correctness | ||
|
|
||
| ## チェック内容 | ||
|
|
||
| `$props()` から分割代入された値のうち `$bindable` を宣言していないものへの変異を検出します — メンバー書き込み(`user.name = …`、`obj.count += 1`)、`delete obj.x`、変異メソッド呼び出し(`items.push(…)`、`arr.splice(…)`、`map.set(…)` など)です。`...rest` で受けたバインディングも対象になります — rest props は個別に `$bindable` を宣言できないためです。prop 自体への単純な再代入(`count = 5`)は対象外です — Svelte の公式ドキュメントは一時的な状態保持のための再代入を明示的に許容しており、禁止されているのは変異のみです。コンポーネントのスクリプトとテンプレートを静的(CLI)解析します。 | ||
|
|
||
| prop と同名の関数パラメータや `{#each ... as x}` のループ変数を変異させても検出対象にはなりません — そのバインディングは prop をシャドーイングしており、もはや prop 自体ではないためです。それ以外の形のシャドーイング(ブロックスコープの `let`/`const` による再宣言、`{#snippet}`/`{:then}`/`{:catch}` のバインディング)は追跡しておらず、理論上は誤検出につながり得ます — これは意図的に部分的な緩和策であり、完全なスコープ解決ではありません。 | ||
|
|
||
| ## なぜ重要か | ||
|
|
||
| Svelte の公式ドキュメントは明確に「`$bindable` でない限り prop を変異させてはいけない」と述べています。コンパイラが捕まえない失敗モードが3つあります: | ||
|
|
||
| - **プレーンオブジェクト**の prop を変異させても、オブジェクトが state proxy でないため**無言で何も起きません**(開発時の警告すら出ません)。 | ||
| - **リアクティブな state proxy** の prop を変異させると動作はしますが、`ownership_invalid_mutation` という開発時警告が出ます — ただしそれは**そのコードパスが実際に実行された場合のみ**です。 | ||
| - 使用中の**フォールバック値**もプレーンオブジェクトと同様に振る舞い、変異は反映されません。 | ||
|
|
||
| 静的解析であれば、コードパスが実行される前のレビュー・CI の時点でこの3つすべてを捕まえられます。 | ||
|
|
||
| ## 修正方法 | ||
|
|
||
| ```svelte | ||
| <script> | ||
| let { user } = $props(); | ||
|
|
||
| // prop を直接変異させる代わりに: | ||
| function rename(name) { | ||
| user.name = name; // 何も起きないか、ownership_invalid_mutation 警告が出る | ||
| } | ||
|
|
||
| // 変異前にクローンする: | ||
| function rename(name) { | ||
| const next = { ...user, name }; | ||
| // next を使うか、変更を親に持ち上げる | ||
| } | ||
|
|
||
| // 親子で共有すべきなら bindable にする: | ||
| let { user = $bindable() } = $props(); | ||
| </script> | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| --- | ||
| title: CORRECT005 · Mutated non-bindable prop | ||
| description: Don't mutate a prop from $props() unless it is declared $bindable. | ||
| --- | ||
|
|
||
| **Severity:** warning · **Category:** correctness | ||
|
|
||
| ## What it checks | ||
|
|
||
| Flags a mutation of a value destructured from `$props()` that is not declared `$bindable`: a member write (`user.name = …`, `obj.count += 1`), `delete obj.x`, or a call to a mutating method (`items.push(…)`, `arr.splice(…)`, `map.set(…)`, …). A `...rest` binding is tracked too — rest props can never be individually declared `$bindable`. Plain reassignment of the prop itself (`count = 5`) is **not** flagged — Svelte's docs explicitly sanction temporary reassignment for unsaved ephemeral state; only mutation is prohibited. Checked by static (CLI) analysis of the component script and template. | ||
|
|
||
| A function parameter or `{#each ... as x}` loop variable that reuses the prop's name is not flagged when mutated — that binding shadows the prop, so it isn't the prop at all. Other forms of shadowing (a block-scoped `let`/`const` redeclaration, `{#snippet}`/`{:then}`/`{:catch}` bindings) are not tracked and could in principle produce a false positive; this is a deliberately partial mitigation, not full scope resolution. | ||
|
|
||
| ## Why it matters | ||
|
|
||
| Svelte's docs say plainly: "don't mutate props" unless they are `$bindable`. Three failure modes, none caught by the compiler: | ||
|
|
||
| - A **plain-object** prop mutation is a silent no-op — the object isn't a state proxy, so not even the dev-time warning fires. | ||
| - A **reactive-state-proxy** prop mutation works, but triggers the `ownership_invalid_mutation` dev warning — only if that code path is actually exercised at runtime. | ||
| - A **fallback value** in use behaves like a plain object — mutation has no effect. | ||
|
|
||
| Static analysis catches all three at review/CI time, before the code path has to run. | ||
|
|
||
| ## How to fix | ||
|
|
||
| ```svelte | ||
| <script> | ||
| let { user } = $props(); | ||
|
|
||
| // Instead of mutating the prop directly: | ||
| function rename(name) { | ||
| user.name = name; // no-op or ownership_invalid_mutation warning | ||
| } | ||
|
|
||
| // Clone before mutating: | ||
| function rename(name) { | ||
| const next = { ...user, name }; | ||
| // ...use `next`, or lift the change to the parent | ||
| } | ||
|
|
||
| // Or make it bindable, if the parent and child should share it: | ||
| let { user = $bindable() } = $props(); | ||
| </script> | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
packages/core/src/rules/correctness/correct005-prop-mutation.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { componentRule } from '../component-rule.js'; | ||
|
|
||
| export const correct005PropMutation = componentRule({ | ||
| id: 'CORRECT005', | ||
| title: 'Mutated non-bindable prop', | ||
| category: 'correctness', | ||
| label: 'Prop mutation', | ||
| recommendation: | ||
| 'Clone the value before mutating it, communicate the change via a callback prop, or declare the prop $bindable if the parent and child should share it.', | ||
| rationale: | ||
| "Svelte's docs say plainly: don't mutate props unless they are $bindable. A plain-object prop mutation is a silent no-op (the object isn't a state proxy); a reactive-state-proxy prop mutation works but triggers the ownership_invalid_mutation dev warning only when that code path actually runs. Neither is caught by the compiler, so this rule catches both statically.", | ||
| applies: (c) => c.mutatedProps.length > 0, | ||
| bad: (c) => | ||
| c.mutatedProps.map((m) => ({ | ||
| line: m.line, | ||
| message: `Prop "${m.name}" is mutated, but it is not declared $bindable` | ||
| })) | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.