Warn on non-destructured `$props()` reads in runes mode (#17708)

Non-destructured `$props()` access in runes mode silently skipped the
`state_referenced_locally` warning, leading to missed guidance when
users read `props` via identifiers or member expressions.

- **Analyzer behavior**
- Include `rest_prop` bindings in `state_referenced_locally` detection
so reads of `$props()` identifiers warn consistently with destructured
props.

- **Validation coverage**
- Add a validator fixture for `$props()` identifiers and update the
`props-identifier` snapshot expectations to capture the new warnings.

Example:

```svelte
<script>
	const props = $props();
	const { model } = props;       // now warns
	const value = props.model.value; // now warns
</script>
```

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>False negative for `state_referenced_locally` warning on
not destructured `$props` access?</issue_title>
> <issue_description>### Describe the bug
> 
> I was looking for a workaround for sveltejs/svelte#17669 and thought
of not destructuring the `$props` directly; to my surprise there were no
warnings at all.
> 
> 
> ### Reproduction
> 
> ```js
> const props = $props();
> const { model } = props; // missing warning
> 
> const value = props.model.value; // missing warning
> ```
> 
>
[Playground](https://svelte.dev/playground/untitled?version=5.50.2#H4sIAAAAAAAACn2QT4vCQAzFv0oIe1CQ9l51YY97lj1tPYxtXAam6TAT_1H63U0HUax1j3nvJeT3OmTTEBb4w2LFUY0L3FtHEYvfDuXiB28QVL8lv7zP4pGcDNrORJrSq5aFWPQMrmIVrJfPkktROQp00LQ1OehhDR8-tD7O5ku174GjcQdSM8WyNC0hz4HOniqhGk4msOW_klf54zrPNkTwzVUbgsZuz8z1G6GzYCHhQP3iDdV47Zltwv2XMEGN6Cbgk5vIGhujAj3AXstI4WxcycvivZFn7q1OxrqT5RqLvXGR-itXywVk_AEAAA)
> 
> ### Logs
> 
> ```shell
> 
> ```
> 
> ### System Info
> 
> ```shell
> REPL - Svelte v.5.50.2
> ```
> 
> ### Severity
> 
> annoyance</issue_description>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> </comments>
> 


</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes sveltejs/svelte#17685

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Rich-Harris <1162160+Rich-Harris@users.noreply.github.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>
Co-authored-by: Paolo Ricciuti <ricciutipaolo@gmail.com>
pull/17711/head
Copilot 3 days ago committed by GitHub
parent 01f1937a98
commit dd9fc0d1ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: emit state_referenced_locally warning for non-destructured props

@ -115,7 +115,8 @@ export function Identifier(node, context) {
!should_proxy(binding.initial.arguments[0], context.state.scope)))) ||
binding.kind === 'raw_state' ||
binding.kind === 'derived' ||
binding.kind === 'prop') &&
binding.kind === 'prop' ||
binding.kind === 'rest_prop') &&
// We're only concerned with reads here
(parent.type !== 'AssignmentExpression' || parent.left !== node) &&
parent.type !== 'UpdateExpression'

@ -0,0 +1,142 @@
[
{
"code": "state_referenced_locally",
"message": "This reference only captures the initial value of `props`. Did you mean to reference it inside a closure instead?\nhttps://svelte.dev/e/state_referenced_locally",
"filename": "packages/svelte/tests/snapshot/samples/props-identifier/index.svelte",
"start": {
"line": 3,
"column": 1,
"character": 33
},
"end": {
"line": 3,
"column": 6,
"character": 38
},
"position": [
33,
38
],
"frame": "1: <script>\n2: let props = $props();\n3: props.a;\n ^\n4: props[a];\n5: props.a.b;"
},
{
"code": "state_referenced_locally",
"message": "This reference only captures the initial value of `props`. Did you mean to reference it inside a closure instead?\nhttps://svelte.dev/e/state_referenced_locally",
"filename": "packages/svelte/tests/snapshot/samples/props-identifier/index.svelte",
"start": {
"line": 4,
"column": 1,
"character": 43
},
"end": {
"line": 4,
"column": 6,
"character": 48
},
"position": [
43,
48
],
"frame": "2: let props = $props();\n3: props.a;\n4: props[a];\n ^\n5: props.a.b;\n6: props.a.b = true;"
},
{
"code": "state_referenced_locally",
"message": "This reference only captures the initial value of `props`. Did you mean to reference it inside a closure instead?\nhttps://svelte.dev/e/state_referenced_locally",
"filename": "packages/svelte/tests/snapshot/samples/props-identifier/index.svelte",
"start": {
"line": 5,
"column": 1,
"character": 54
},
"end": {
"line": 5,
"column": 6,
"character": 59
},
"position": [
54,
59
],
"frame": "3: props.a;\n4: props[a];\n5: props.a.b;\n ^\n6: props.a.b = true;\n7: props.a = true;"
},
{
"code": "state_referenced_locally",
"message": "This reference only captures the initial value of `props`. Did you mean to reference it inside a closure instead?\nhttps://svelte.dev/e/state_referenced_locally",
"filename": "packages/svelte/tests/snapshot/samples/props-identifier/index.svelte",
"start": {
"line": 6,
"column": 1,
"character": 66
},
"end": {
"line": 6,
"column": 6,
"character": 71
},
"position": [
66,
71
],
"frame": "4: props[a];\n5: props.a.b;\n6: props.a.b = true;\n ^\n7: props.a = true;\n8: props[a] = true;"
},
{
"code": "state_referenced_locally",
"message": "This reference only captures the initial value of `props`. Did you mean to reference it inside a closure instead?\nhttps://svelte.dev/e/state_referenced_locally",
"filename": "packages/svelte/tests/snapshot/samples/props-identifier/index.svelte",
"start": {
"line": 7,
"column": 1,
"character": 85
},
"end": {
"line": 7,
"column": 6,
"character": 90
},
"position": [
85,
90
],
"frame": " 5: props.a.b;\n 6: props.a.b = true;\n 7: props.a = true;\n ^\n 8: props[a] = true;\n 9: props;"
},
{
"code": "state_referenced_locally",
"message": "This reference only captures the initial value of `props`. Did you mean to reference it inside a closure instead?\nhttps://svelte.dev/e/state_referenced_locally",
"filename": "packages/svelte/tests/snapshot/samples/props-identifier/index.svelte",
"start": {
"line": 8,
"column": 1,
"character": 102
},
"end": {
"line": 8,
"column": 6,
"character": 107
},
"position": [
102,
107
],
"frame": " 6: props.a.b = true;\n 7: props.a = true;\n 8: props[a] = true;\n ^\n 9: props;\n10: </script>"
},
{
"code": "state_referenced_locally",
"message": "This reference only captures the initial value of `props`. Did you mean to reference it inside a closure instead?\nhttps://svelte.dev/e/state_referenced_locally",
"filename": "packages/svelte/tests/snapshot/samples/props-identifier/index.svelte",
"start": {
"line": 9,
"column": 1,
"character": 120
},
"end": {
"line": 9,
"column": 6,
"character": 125
},
"position": [
120,
125
],
"frame": " 7: props.a = true;\n 8: props[a] = true;\n 9: props;\n ^\n10: </script>\n11: "
}
]

@ -0,0 +1,142 @@
[
{
"code": "state_referenced_locally",
"message": "This reference only captures the initial value of `props`. Did you mean to reference it inside a closure instead?\nhttps://svelte.dev/e/state_referenced_locally",
"filename": "packages/svelte/tests/snapshot/samples/props-identifier/index.svelte",
"start": {
"line": 3,
"column": 1,
"character": 33
},
"end": {
"line": 3,
"column": 6,
"character": 38
},
"position": [
33,
38
],
"frame": "1: <script>\n2: let props = $props();\n3: props.a;\n ^\n4: props[a];\n5: props.a.b;"
},
{
"code": "state_referenced_locally",
"message": "This reference only captures the initial value of `props`. Did you mean to reference it inside a closure instead?\nhttps://svelte.dev/e/state_referenced_locally",
"filename": "packages/svelte/tests/snapshot/samples/props-identifier/index.svelte",
"start": {
"line": 4,
"column": 1,
"character": 43
},
"end": {
"line": 4,
"column": 6,
"character": 48
},
"position": [
43,
48
],
"frame": "2: let props = $props();\n3: props.a;\n4: props[a];\n ^\n5: props.a.b;\n6: props.a.b = true;"
},
{
"code": "state_referenced_locally",
"message": "This reference only captures the initial value of `props`. Did you mean to reference it inside a closure instead?\nhttps://svelte.dev/e/state_referenced_locally",
"filename": "packages/svelte/tests/snapshot/samples/props-identifier/index.svelte",
"start": {
"line": 5,
"column": 1,
"character": 54
},
"end": {
"line": 5,
"column": 6,
"character": 59
},
"position": [
54,
59
],
"frame": "3: props.a;\n4: props[a];\n5: props.a.b;\n ^\n6: props.a.b = true;\n7: props.a = true;"
},
{
"code": "state_referenced_locally",
"message": "This reference only captures the initial value of `props`. Did you mean to reference it inside a closure instead?\nhttps://svelte.dev/e/state_referenced_locally",
"filename": "packages/svelte/tests/snapshot/samples/props-identifier/index.svelte",
"start": {
"line": 6,
"column": 1,
"character": 66
},
"end": {
"line": 6,
"column": 6,
"character": 71
},
"position": [
66,
71
],
"frame": "4: props[a];\n5: props.a.b;\n6: props.a.b = true;\n ^\n7: props.a = true;\n8: props[a] = true;"
},
{
"code": "state_referenced_locally",
"message": "This reference only captures the initial value of `props`. Did you mean to reference it inside a closure instead?\nhttps://svelte.dev/e/state_referenced_locally",
"filename": "packages/svelte/tests/snapshot/samples/props-identifier/index.svelte",
"start": {
"line": 7,
"column": 1,
"character": 85
},
"end": {
"line": 7,
"column": 6,
"character": 90
},
"position": [
85,
90
],
"frame": " 5: props.a.b;\n 6: props.a.b = true;\n 7: props.a = true;\n ^\n 8: props[a] = true;\n 9: props;"
},
{
"code": "state_referenced_locally",
"message": "This reference only captures the initial value of `props`. Did you mean to reference it inside a closure instead?\nhttps://svelte.dev/e/state_referenced_locally",
"filename": "packages/svelte/tests/snapshot/samples/props-identifier/index.svelte",
"start": {
"line": 8,
"column": 1,
"character": 102
},
"end": {
"line": 8,
"column": 6,
"character": 107
},
"position": [
102,
107
],
"frame": " 6: props.a.b = true;\n 7: props.a = true;\n 8: props[a] = true;\n ^\n 9: props;\n10: </script>"
},
{
"code": "state_referenced_locally",
"message": "This reference only captures the initial value of `props`. Did you mean to reference it inside a closure instead?\nhttps://svelte.dev/e/state_referenced_locally",
"filename": "packages/svelte/tests/snapshot/samples/props-identifier/index.svelte",
"start": {
"line": 9,
"column": 1,
"character": 120
},
"end": {
"line": 9,
"column": 6,
"character": 125
},
"position": [
120,
125
],
"frame": " 7: props.a = true;\n 8: props[a] = true;\n 9: props;\n ^\n10: </script>\n11: "
}
]

@ -0,0 +1,6 @@
<script>
const props = $props();
const { model } = props;
const value = props.model.value;
console.log(model, value);
</script>

@ -0,0 +1,26 @@
[
{
"code": "state_referenced_locally",
"message": "This reference only captures the initial value of `props`. Did you mean to reference it inside a closure instead?",
"start": {
"line": 3,
"column": 19
},
"end": {
"line": 3,
"column": 24
}
},
{
"code": "state_referenced_locally",
"message": "This reference only captures the initial value of `props`. Did you mean to reference it inside a closure instead?",
"start": {
"line": 4,
"column": 15
},
"end": {
"line": 4,
"column": 20
}
}
]
Loading…
Cancel
Save