Repository: sveltejs/svelte (JavaScript)
Date: 2022-09-12 09:51:00+00:00
Task type: Bug Fix

Issue:
Invalidating functions cannot be declared as `@const`
### Describe the bug

If a function is defined via `@const` that causes invalidation of state, this will throw an error because `$$invalidate` will not be in scope.

Possibly a known/intended limitation.

### Reproduction

```svelte
<script>
	let items = [
		{ name: 'A', selected: true },
		{ name: 'B', selected: false },
		{ name: 'C', selected: false },
	]
</script>

{#each items as item}
	{@const toggle = () => item.selected = !item.selected}
	<div>
		{item.selected ? '☑' : '☐'}
		{item.name}
		<button on:click={toggle}>Toggle</button>
	</div>
{/each}
```

[REPL](https://svelte.dev/repl/fc201eac554e4ef89256a46e49efdee2?version=3.50.0)


### Logs

```shell
Uncaught ReferenceError: $$invalidate is not defined
    at HTMLButtonElement.constants_0 (eval at handle_message (about:srcdoc:14:8), <anonymous>:389:32)
    at HTMLButtonElement.eval (eval at handle_message (about:srcdoc:14:8), <anonymous>:435:65)
```


### System Info

```shell
REPL / Chrome & Firefox
```


### Severity

annoyance


Issue discussion:

Solution patch:
diff --git a/src/compiler/compile/nodes/shared/Expression.ts b/src/compiler/compile/nodes/shared/Expression.ts
--- a/src/compiler/compile/nodes/shared/Expression.ts
+++ b/src/compiler/compile/nodes/shared/Expression.ts
@@ -252,41 +252,7 @@ export default class Expression {
 					);
 
 					const declaration = b`const ${id} = ${node}`;
-
-					if (owner.type === 'ConstTag') {
-						let child_scope = scope;
-						walk(node, {
-							enter(node: Node, parent: any) {
-								if (map.has(node)) child_scope = map.get(node);
-								if (node.type === 'Identifier' && is_reference(node, parent)) {
-									if (child_scope.has(node.name)) return;
-									this.replace(block.renderer.reference(node, ctx));
-								}
-							},
-							leave(node: Node) {
-								if (map.has(node)) child_scope = child_scope.parent;
-							}
-						});
-					} else if (dependencies.size === 0 && contextual_dependencies.size === 0) {
-						// we can hoist this out of the component completely
-						component.fully_hoisted.push(declaration);
-
-						this.replace(id as any);
-
-						component.add_var(node, {
-							name: id.name,
-							internal: true,
-							hoistable: true,
-							referenced: true
-						});
-					} else if (contextual_dependencies.size === 0) {
-						// function can be hoisted inside the component init
-						component.partly_hoisted.push(declaration);
-
-						block.renderer.add_to_context(id.name);
-						this.replace(block.renderer.reference(id));
-					} else {
-						// we need a combo block/init recipe
+					const extract_functions = () => {
 						const deps = Array.from(contextual_dependencies);
 						const function_expression = node as FunctionExpression;
 
@@ -296,7 +262,7 @@ export default class Expression {
 							...function_expression.params
 						];
 
-						const context_args = deps.map(name => block.renderer.reference(name));
+						const context_args = deps.map(name => block.renderer.reference(name, ctx));
 
 						component.partly_hoisted.push(declaration);
 
@@ -312,6 +278,50 @@ export default class Expression {
 							: b`function ${id}() {
 								return ${callee}(${context_args});
 							}`;
+						return { deps, func_declaration };
+					};
+
+					if (owner.type === 'ConstTag') {
+						// we need a combo block/init recipe
+						if (contextual_dependencies.size === 0) {
+							let child_scope = scope;
+							walk(node, {
+								enter(node: Node, parent: any) {
+									if (map.has(node)) child_scope = map.get(node);
+									if (node.type === 'Identifier' && is_reference(node, parent)) {
+										if (child_scope.has(node.name)) return;
+										this.replace(block.renderer.reference(node, ctx));
+									}
+								},
+								leave(node: Node) {
+									if (map.has(node)) child_scope = child_scope.parent;
+								}
+							});
+						} else {
+							const { func_declaration } = extract_functions();
+							this.replace(func_declaration[0]);
+						}
+					} else if (dependencies.size === 0 && contextual_dependencies.size === 0) {
+						// we can hoist this out of the component completely
+						component.fully_hoisted.push(declaration);
+
+						this.replace(id as any);
+
+						component.add_var(node, {
+							name: id.name,
+							internal: true,
+							hoistable: true,
+							referenced: true
+						});
+					} else if (contextual_dependencies.size === 0) {
+						// function can be hoisted inside the component init
+						component.partly_hoisted.push(declaration);
+
+						block.renderer.add_to_context(id.name);
+						this.replace(block.renderer.reference(id));
+					} else {
+						// we need a combo block/init recipe
+						const { deps, func_declaration } = extract_functions();
 
 						if (owner.type === 'Attribute' && owner.parent.name === 'slot') {
 							const dep_scopes = new Set<INode>(deps.map(name => template_scope.get_owner(name)));


Test patch:
diff --git a/test/runtime/samples/const-tag-invalidate/_config.js b/test/runtime/samples/const-tag-invalidate/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/const-tag-invalidate/_config.js
@@ -0,0 +1,34 @@
+export default {
+	html: `
+		<div>[Y] A <button>Toggle</button></div>
+		<div>[N] B <button>Toggle</button></div>
+		<div>[N] C <button>Toggle</button></div>
+	`,
+	async test({ component, target, assert, window }) {
+		const [btn1, btn2, btn3] = target.querySelectorAll('button');
+		await btn1.dispatchEvent(new window.MouseEvent('click'));
+		await btn2.dispatchEvent(new window.MouseEvent('click'));
+
+		assert.htmlEqual(target.innerHTML, `
+			<div>[N] A <button>Toggle</button></div>
+			<div>[Y] B <button>Toggle</button></div>
+			<div>[N] C <button>Toggle</button></div>
+		`);
+
+		await btn2.dispatchEvent(new window.MouseEvent('click'));
+
+		assert.htmlEqual(target.innerHTML, `
+			<div>[N] A <button>Toggle</button></div>
+			<div>[N] B <button>Toggle</button></div>
+			<div>[N] C <button>Toggle</button></div>
+		`);
+
+		await btn3.dispatchEvent(new window.MouseEvent('click'));
+
+		assert.htmlEqual(target.innerHTML, `
+			<div>[N] A <button>Toggle</button></div>
+			<div>[N] B <button>Toggle</button></div>
+			<div>[Y] C <button>Toggle</button></div>
+		`);
+	}
+};
diff --git a/test/runtime/samples/const-tag-invalidate/main.svelte b/test/runtime/samples/const-tag-invalidate/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/const-tag-invalidate/main.svelte
@@ -0,0 +1,16 @@
+<script>
+	let items = [
+		{ name: 'A', selected: true },
+		{ name: 'B', selected: false },
+		{ name: 'C', selected: false },
+	]
+</script>
+
+{#each items as item}
+	{@const toggle = () => item.selected = !item.selected}
+	<div>
+		{item.selected ? '[Y]' : '[N]'}
+		{item.name}
+		<button on:click={toggle}>Toggle</button>
+	</div>
+{/each}