feat: warn on referenced mutated nonstate (#9669)

Walk the path and warn if this is a mutated normal variable that's referenced inside a function scope
pull/9650/head
Rich Harris 1 year ago committed by GitHub
parent 9c44fd7854
commit 6e863e617c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: warn on references to mutated non-state in template

@ -218,7 +218,7 @@ export function analyze_module(ast, options) {
for (const [, scope] of scopes) { for (const [, scope] of scopes) {
for (const [name, binding] of scope.declarations) { for (const [name, binding] of scope.declarations) {
if (binding.kind === 'state' && !binding.mutated) { if (binding.kind === 'state' && !binding.mutated) {
warn(warnings, binding.node, [], 'state-rune-not-mutated', name); warn(warnings, binding.node, [], 'state-not-mutated', name);
} }
} }
} }
@ -377,7 +377,7 @@ export function analyze_component(root, options) {
for (const [, scope] of instance.scopes) { for (const [, scope] of instance.scopes) {
for (const [name, binding] of scope.declarations) { for (const [name, binding] of scope.declarations) {
if (binding.kind === 'state' && !binding.mutated) { if (binding.kind === 'state' && !binding.mutated) {
warn(warnings, binding.node, [], 'state-rune-not-mutated', name); warn(warnings, binding.node, [], 'state-not-mutated', name);
} }
} }
} }
@ -414,6 +414,30 @@ export function analyze_component(root, options) {
analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements); analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements);
} }
// warn on any nonstate declarations that are a) mutated and b) referenced in the template
for (const scope of [module.scope, instance.scope]) {
outer: for (const [name, binding] of scope.declarations) {
if (binding.kind === 'normal' && binding.mutated) {
for (const { path } of binding.references) {
if (path[0].type !== 'Fragment') continue;
for (let i = 1; i < path.length; i += 1) {
const type = path[i].type;
if (
type === 'FunctionDeclaration' ||
type === 'FunctionExpression' ||
type === 'ArrowFunctionExpression'
) {
continue;
}
}
warn(warnings, binding.node, [], 'non-state-reference', name);
continue outer;
}
}
}
}
analysis.stylesheet.validate(analysis); analysis.stylesheet.validate(analysis);
for (const element of analysis.elements) { for (const element of analysis.elements) {

@ -175,13 +175,13 @@ export class Scope {
references.push({ node, path }); references.push({ node, path });
const declaration = this.declarations.get(node.name); const binding = this.declarations.get(node.name);
if (declaration) { if (binding) {
declaration.references.push({ node, path }); binding.references.push({ node, path });
} else if (this.#parent) { } else if (this.#parent) {
this.#parent.reference(node, path); this.#parent.reference(node, path);
} else { } else {
// no declaration was found, and this is the top level scope, // no binding was found, and this is the top level scope,
// which means this is a global // which means this is a global
this.root.conflicts.add(node.name); this.root.conflicts.add(node.name);
} }

@ -22,8 +22,11 @@ const runes = {
`It looks like you're using the $${name} rune, but there is a local binding called ${name}. ` + `It looks like you're using the $${name} rune, but there is a local binding called ${name}. ` +
`Referencing a local variable with a $ prefix will create a store subscription. Please rename ${name} to avoid the ambiguity.`, `Referencing a local variable with a $ prefix will create a store subscription. Please rename ${name} to avoid the ambiguity.`,
/** @param {string} name */ /** @param {string} name */
'state-rune-not-mutated': (name) => 'state-not-mutated': (name) =>
`${name} is declared with $state(...) but is never updated. Did you mean to create a function that changes its value?` `${name} is declared with $state(...) but is never updated. Did you mean to create a function that changes its value?`,
/** @param {string} name */
'non-state-reference': (name) =>
`${name} is updated, but is not declared with $state(...). Changing its value will not correctly trigger updates.`
}; };
/** @satisfies {Warnings} */ /** @satisfies {Warnings} */

@ -0,0 +1,3 @@
import { test } from '../../test';
export default test({});

@ -0,0 +1,10 @@
<script>
let a = $state(1);
let b = 2;
let c = 3;
</script>
<button onclick={() => a += 1}>a += 1</button>
<button onclick={() => b += 1}>b += 1</button>
<button onclick={() => c += 1}>c += 1</button>
<p>{a} + {b} + {c} = {a + b + c}</p>

@ -0,0 +1,26 @@
[
{
"code": "non-state-reference",
"message": "b is updated, but is not declared with $state(...). Changing its value will not correctly trigger updates.",
"start": {
"column": 5,
"line": 3
},
"end": {
"column": 6,
"line": 3
}
},
{
"code": "non-state-reference",
"message": "c is updated, but is not declared with $state(...). Changing its value will not correctly trigger updates.",
"start": {
"column": 5,
"line": 4
},
"end": {
"column": 6,
"line": 4
}
}
]

@ -1,6 +1,6 @@
[ [
{ {
"code": "state-rune-not-mutated", "code": "state-not-mutated",
"end": { "end": {
"column": 11, "column": 11,
"line": 3 "line": 3

Loading…
Cancel
Save