diff --git a/.changeset/social-pianos-appear.md b/.changeset/social-pianos-appear.md new file mode 100644 index 0000000000..953d963615 --- /dev/null +++ b/.changeset/social-pianos-appear.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: add warning for non-updated bindable diff --git a/documentation/docs/98-reference/.generated/compile-warnings.md b/documentation/docs/98-reference/.generated/compile-warnings.md index 7dfbe75888..3a5d0fb437 100644 --- a/documentation/docs/98-reference/.generated/compile-warnings.md +++ b/documentation/docs/98-reference/.generated/compile-warnings.md @@ -600,6 +600,14 @@ Bidirectional control characters can alter the direction in which text appears t The rest operator (...) will create a new object and binding '%name%' with the original object will not work ``` +### bindable_prop_not_mutated + +``` +`%name%` is declared with `$bindable()` but is not mutated or reassigned +``` + +The `$bindable()` rune marks a prop as two-way bindable, meaning the component can update the value and the parent will see the changes. If the prop is never mutated or reassigned within the component, `$bindable()` has no effect and should be removed. + ### block_empty ``` diff --git a/packages/svelte/messages/compile-warnings/script.md b/packages/svelte/messages/compile-warnings/script.md index 26bd0c9027..3ea738668d 100644 --- a/packages/svelte/messages/compile-warnings/script.md +++ b/packages/svelte/messages/compile-warnings/script.md @@ -1,3 +1,9 @@ +## bindable_prop_not_mutated + +> `%name%` is declared with `$bindable()` but is not mutated or reassigned + +The `$bindable()` rune marks a prop as two-way bindable, meaning the component can update the value and the parent will see the changes. If the prop is never mutated or reassigned within the component, `$bindable()` has no effect and should be removed. + ## custom_element_props_identifier > Using a rest element or a non-destructured declaration with `$props()` means that Svelte can't infer what properties to expose when creating a custom element. Consider destructuring all the props or explicitly specifying the `customElement.props` option. diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index ef0b35f560..b01858690e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -764,6 +764,9 @@ export function analyze_component(root, source, options) { w.non_reactive_update(binding.node, name); continue outer; } + } else if (binding.kind === 'bindable_prop' && !binding.updated) { + // this check only applies to instance but we check here to avoid a second loop + w.bindable_prop_not_mutated(binding.node, name); } } } diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js index 089cb1e118..8e8d536c33 100644 --- a/packages/svelte/src/compiler/warnings.js +++ b/packages/svelte/src/compiler/warnings.js @@ -91,6 +91,7 @@ export const codes = [ 'options_removed_hydratable', 'options_removed_loop_guard_timeout', 'options_renamed_ssr_dom', + 'bindable_prop_not_mutated', 'custom_element_props_identifier', 'export_let_unused', 'legacy_component_creation', @@ -594,6 +595,15 @@ export function options_renamed_ssr_dom(node) { w(node, 'options_renamed_ssr_dom', `\`generate: "dom"\` and \`generate: "ssr"\` options have been renamed to "client" and "server" respectively\nhttps://svelte.dev/e/options_renamed_ssr_dom`); } +/** + * `%name%` is declared with `$bindable()` but is not mutated or reassigned + * @param {null | NodeLike} node + * @param {string} name + */ +export function bindable_prop_not_mutated(node, name) { + w(node, 'bindable_prop_not_mutated', `\`${name}\` is declared with \`$bindable()\` but is not mutated or reassigned\nhttps://svelte.dev/e/bindable_prop_not_mutated`); +} + /** * Using a rest element or a non-destructured declaration with `$props()` means that Svelte can't infer what properties to expose when creating a custom element. Consider destructuring all the props or explicitly specifying the `customElement.props` option. * @param {null | NodeLike} node diff --git a/packages/svelte/tests/validator/samples/bindable-prop-not-mutated/input.svelte b/packages/svelte/tests/validator/samples/bindable-prop-not-mutated/input.svelte new file mode 100644 index 0000000000..3492815c78 --- /dev/null +++ b/packages/svelte/tests/validator/samples/bindable-prop-not-mutated/input.svelte @@ -0,0 +1,12 @@ + + +

{foo} {bar} {baz.nested}

diff --git a/packages/svelte/tests/validator/samples/bindable-prop-not-mutated/warnings.json b/packages/svelte/tests/validator/samples/bindable-prop-not-mutated/warnings.json new file mode 100644 index 0000000000..4fcd90b6be --- /dev/null +++ b/packages/svelte/tests/validator/samples/bindable-prop-not-mutated/warnings.json @@ -0,0 +1,14 @@ +[ + { + "code": "bindable_prop_not_mutated", + "end": { + "column": 5, + "line": 3 + }, + "message": "`foo` is declared with `$bindable()` but is not mutated or reassigned", + "start": { + "column": 2, + "line": 3 + } + } +] diff --git a/packages/svelte/tests/validator/samples/svelte-ignore-bindable-prop-not-mutated/input.svelte b/packages/svelte/tests/validator/samples/svelte-ignore-bindable-prop-not-mutated/input.svelte new file mode 100644 index 0000000000..3be0c07989 --- /dev/null +++ b/packages/svelte/tests/validator/samples/svelte-ignore-bindable-prop-not-mutated/input.svelte @@ -0,0 +1,6 @@ + + +

{foo}

diff --git a/packages/svelte/tests/validator/samples/svelte-ignore-bindable-prop-not-mutated/warnings.json b/packages/svelte/tests/validator/samples/svelte-ignore-bindable-prop-not-mutated/warnings.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/packages/svelte/tests/validator/samples/svelte-ignore-bindable-prop-not-mutated/warnings.json @@ -0,0 +1 @@ +[]