Repository: sveltejs/svelte (JavaScript) — Bug Fix

Issue:
scope issue in v3
pngwn says this might be a bug so I'm filing an issue here. Please close if that doesn't apply.

Using more than one each loop and referring to the other enumerator in a function returns a `func not defined` error. There is a repl example for that: https://v3.svelte.technology/repl?version=3.0.0-alpha13&gist=ff5d3d21d1ef50f256b3460af2fa36c1

pngwn suggested a workaround which does the job but is less obvious as to what it actually does: https://v3.svelte.technology/repl?version=3.0.0-alpha13&gist=24aaba69ba36f806f14cf2b03c190a35



Discussion:

Resolving patch:
diff --git a/src/compile/nodes/EventHandler.ts b/src/compile/nodes/EventHandler.ts
--- a/src/compile/nodes/EventHandler.ts
+++ b/src/compile/nodes/EventHandler.ts
@@ -2,6 +2,7 @@ import Node from './shared/Node';
 import Expression from './shared/Expression';
 import Component from '../Component';
 import deindent from '../../utils/deindent';
+import Block from '../render-dom/Block';
 
 export default class EventHandler extends Node {
 	name: string;
@@ -52,8 +53,9 @@ export default class EventHandler extends Node {
 		}
 	}
 
-	render() {
-		if (this.expression) return this.expression.render();
+	// TODO move this? it is specific to render-dom
+	render(block: Block) {
+		if (this.expression) return this.expression.render(block);
 
 		this.component.template_references.add(this.handler_name);
 		return `ctx.${this.handler_name}`;
diff --git a/src/compile/nodes/shared/Expression.ts b/src/compile/nodes/shared/Expression.ts
--- a/src/compile/nodes/shared/Expression.ts
+++ b/src/compile/nodes/shared/Expression.ts
@@ -11,6 +11,7 @@ import sanitize from '../../../utils/sanitize';
 import TemplateScope from './TemplateScope';
 import getObject from '../../../utils/getObject';
 import { nodes_match } from '../../../utils/nodes_match';
+import Block from '../../render-dom/Block';
 
 const binaryOperators: Record<string, number> = {
 	'**': 15,
@@ -86,13 +87,6 @@ export default class Expression {
 		Object.defineProperties(this, {
 			component: {
 				value: component
-			},
-
-			// TODO remove this, is just for debugging
-			snippet: {
-				get: () => {
-					throw new Error(`cannot access expression.snippet, use expression.render() instead`)
-				}
 			}
 		});
 
@@ -181,7 +175,7 @@ export default class Expression {
 	}
 
 	// TODO move this into a render-dom wrapper?
-	render() {
+	render(block: Block) {
 		if (this.rendered) return this.rendered;
 
 		const {
@@ -407,6 +401,13 @@ export default class Expression {
 			}
 		});
 
+		if (declarations.length > 0) {
+			block.maintainContext = true;
+			declarations.forEach(declaration => {
+				block.builders.init.addBlock(declaration);
+			});
+		}
+
 		return this.rendered = `[✂${this.node.start}-${this.node.end}✂]`;
 	}
 }
diff --git a/src/compile/render-dom/wrappers/AwaitBlock.ts b/src/compile/render-dom/wrappers/AwaitBlock.ts
--- a/src/compile/render-dom/wrappers/AwaitBlock.ts
+++ b/src/compile/render-dom/wrappers/AwaitBlock.ts
@@ -125,7 +125,7 @@ export default class AwaitBlockWrapper extends Wrapper {
 		const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes);
 		const updateMountNode = this.getUpdateMountNode(anchor);
 
-		const snippet = this.node.expression.render();
+		const snippet = this.node.expression.render(block);
 
 		const info = block.getUniqueName(`info`);
 		const promise = block.getUniqueName(`promise`);
diff --git a/src/compile/render-dom/wrappers/Document.ts b/src/compile/render-dom/wrappers/Document.ts
--- a/src/compile/render-dom/wrappers/Document.ts
+++ b/src/compile/render-dom/wrappers/Document.ts
@@ -8,7 +8,7 @@ export default class DocumentWrapper extends Wrapper {
 
 	render(block: Block, parentNode: string, parentNodes: string) {
 		this.node.handlers.forEach(handler => {
-			const snippet = handler.render();
+			const snippet = handler.render(block);
 
 			block.builders.init.addBlock(deindent`
 				document.addEventListener("${handler.name}", ${snippet});
diff --git a/src/compile/render-dom/wrappers/EachBlock.ts b/src/compile/render-dom/wrappers/EachBlock.ts
--- a/src/compile/render-dom/wrappers/EachBlock.ts
+++ b/src/compile/render-dom/wrappers/EachBlock.ts
@@ -180,7 +180,7 @@ export default class EachBlockWrapper extends Wrapper {
 		if (this.hasBinding) this.contextProps.push(`child_ctx.${this.vars.each_block_value} = list;`);
 		if (this.hasBinding || this.node.index) this.contextProps.push(`child_ctx.${this.indexName} = i;`);
 
-		const snippet = this.node.expression.render();
+		const snippet = this.node.expression.render(block);
 
 		block.builders.init.addLine(`var ${this.vars.each_block_value} = ${snippet};`);
 
diff --git a/src/compile/render-dom/wrappers/Element/Binding.ts b/src/compile/render-dom/wrappers/Element/Binding.ts
--- a/src/compile/render-dom/wrappers/Element/Binding.ts
+++ b/src/compile/render-dom/wrappers/Element/Binding.ts
@@ -65,7 +65,7 @@ export default class BindingWrapper {
 		// view to model
 		this.handler = getEventHandler(this, parent.renderer, block, this.object, contextless_snippet);
 
-		this.snippet = this.node.expression.render();
+		this.snippet = this.node.expression.render(block);
 
 		const type = parent.node.getStaticAttributeValue('type');
 
diff --git a/src/compile/render-dom/wrappers/Element/index.ts b/src/compile/render-dom/wrappers/Element/index.ts
--- a/src/compile/render-dom/wrappers/Element/index.ts
+++ b/src/compile/render-dom/wrappers/Element/index.ts
@@ -534,7 +534,7 @@ export default class ElementWrapper extends Wrapper {
 					: null;
 
 				if (attr.isSpread) {
-					const snippet = attr.expression.render();
+					const snippet = attr.expression.render(block);
 
 					initialProps.push(snippet);
 
@@ -584,7 +584,7 @@ export default class ElementWrapper extends Wrapper {
 		if (intro === outro) {
 			const name = block.getUniqueName(`${this.var}_transition`);
 			const snippet = intro.expression
-				? intro.expression.render()
+				? intro.expression.render(block)
 				: '{}';
 
 			block.addVariable(name);
@@ -616,7 +616,7 @@ export default class ElementWrapper extends Wrapper {
 			if (intro) {
 				block.addVariable(introName);
 				const snippet = intro.expression
-					? intro.expression.render()
+					? intro.expression.render(block)
 					: '{}';
 
 				const fn = component.qualify(intro.name); // TODO add built-in transitions?
@@ -639,7 +639,7 @@ export default class ElementWrapper extends Wrapper {
 			if (outro) {
 				block.addVariable(outroName);
 				const snippet = outro.expression
-					? outro.expression.render()
+					? outro.expression.render(block)
 					: '{}';
 
 				const fn = component.qualify(outro.name);
@@ -680,7 +680,7 @@ export default class ElementWrapper extends Wrapper {
 			${stop_animation}();
 		`);
 
-		const params = this.node.animation.expression ? this.node.animation.expression.render() : '{}';
+		const params = this.node.animation.expression ? this.node.animation.expression.render(block) : '{}';
 
 		const name = component.qualify(this.node.animation.name);
 
@@ -699,7 +699,7 @@ export default class ElementWrapper extends Wrapper {
 			const { expression, name } = classDir;
 			let snippet, dependencies;
 			if (expression) {
-				snippet = expression.render();
+				snippet = expression.render(block);
 				dependencies = expression.dependencies;
 			} else {
 				snippet = `${quotePropIfNecessary(name)}`;
diff --git a/src/compile/render-dom/wrappers/IfBlock.ts b/src/compile/render-dom/wrappers/IfBlock.ts
--- a/src/compile/render-dom/wrappers/IfBlock.ts
+++ b/src/compile/render-dom/wrappers/IfBlock.ts
@@ -32,7 +32,7 @@ class IfBlockBranch extends Wrapper {
 	) {
 		super(renderer, block, parent, node);
 
-		this.condition = (<IfBlock>node).expression && (<IfBlock>node).expression.render();
+		this.condition = (<IfBlock>node).expression && (<IfBlock>node).expression.render(block);
 
 		this.block = block.child({
 			comment: createDebuggingComment(node, parent.renderer.component),
diff --git a/src/compile/render-dom/wrappers/InlineComponent/index.ts b/src/compile/render-dom/wrappers/InlineComponent/index.ts
--- a/src/compile/render-dom/wrappers/InlineComponent/index.ts
+++ b/src/compile/render-dom/wrappers/InlineComponent/index.ts
@@ -150,7 +150,7 @@ export default class InlineComponentWrapper extends Wrapper {
 						: null;
 
 					if (attr.isSpread) {
-						const value = attr.expression.render();
+						const value = attr.expression.render(block);
 						initialProps.push(value);
 
 						changes.push(condition ? `${condition} && ${value}` : value);
@@ -238,7 +238,7 @@ export default class InlineComponentWrapper extends Wrapper {
 			const updating = block.getUniqueName(`updating_${binding.name}`);
 			block.addVariable(updating);
 
-			const snippet = binding.expression.render();
+			const snippet = binding.expression.render(block);
 
 			statements.push(deindent`
 				if (${snippet} !== void 0) {
@@ -305,13 +305,7 @@ export default class InlineComponentWrapper extends Wrapper {
 
 		const munged_handlers = this.node.handlers.map(handler => {
 			// TODO return declarations from handler.render()?
-			const snippet = handler.render();
-
-			if (handler.expression) {
-				handler.expression.declarations.forEach(declaration => {
-					block.builders.init.addBlock(declaration);
-				});
-			}
+			const snippet = handler.render(block);
 
 			return `${name}.$on("${handler.name}", ${snippet});`;
 		});
@@ -320,7 +314,7 @@ export default class InlineComponentWrapper extends Wrapper {
 			const switch_value = block.getUniqueName('switch_value');
 			const switch_props = block.getUniqueName('switch_props');
 
-			const snippet = this.node.expression.render();
+			const snippet = this.node.expression.render(block);
 
 			block.builders.init.addBlock(deindent`
 				var ${switch_value} = ${snippet};
diff --git a/src/compile/render-dom/wrappers/Title.ts b/src/compile/render-dom/wrappers/Title.ts
--- a/src/compile/render-dom/wrappers/Title.ts
+++ b/src/compile/render-dom/wrappers/Title.ts
@@ -33,7 +33,7 @@ export default class TitleWrapper extends Wrapper {
 			if (this.node.children.length === 1) {
 				// single {tag} — may be a non-string
 				const { expression } = this.node.children[0];
-				value = expression.render();
+				value = expression.render(block);
 				addToSet(allDependencies, expression.dynamic_dependencies);
 			} else {
 				// '{foo} {bar}' — treat as string concatenation
@@ -44,7 +44,7 @@ export default class TitleWrapper extends Wrapper {
 							if (chunk.type === 'Text') {
 								return stringify(chunk.data);
 							} else {
-								const snippet = chunk.expression.render();
+								const snippet = chunk.expression.render(block);
 
 								chunk.expression.dynamic_dependencies.forEach(d => {
 									allDependencies.add(d);
diff --git a/src/compile/render-dom/wrappers/shared/Tag.ts b/src/compile/render-dom/wrappers/shared/Tag.ts
--- a/src/compile/render-dom/wrappers/shared/Tag.ts
+++ b/src/compile/render-dom/wrappers/shared/Tag.ts
@@ -19,7 +19,7 @@ export default class Tag extends Wrapper {
 		update: ((value: string) => string)
 	) {
 		const dependencies = this.node.expression.dynamic_dependencies;
-		const snippet = this.node.expression.render();
+		const snippet = this.node.expression.render(block);
 
 		const value = this.node.shouldCache && block.getUniqueName(`${this.var}_value`);
 		const content = this.node.shouldCache ? value : snippet;
diff --git a/src/compile/render-dom/wrappers/shared/addActions.ts b/src/compile/render-dom/wrappers/shared/addActions.ts
--- a/src/compile/render-dom/wrappers/shared/addActions.ts
+++ b/src/compile/render-dom/wrappers/shared/addActions.ts
@@ -12,13 +12,10 @@ export default function addActions(
 	actions.forEach(action => {
 		const { expression } = action;
 		let snippet, dependencies;
+
 		if (expression) {
-			snippet = expression.render();
+			snippet = expression.render(block);
 			dependencies = expression.dynamic_dependencies;
-
-			expression.declarations.forEach(declaration => {
-				block.builders.init.addBlock(declaration);
-			});
 		}
 
 		const name = block.getUniqueName(
diff --git a/src/compile/render-dom/wrappers/shared/addEventHandlers.ts b/src/compile/render-dom/wrappers/shared/addEventHandlers.ts
--- a/src/compile/render-dom/wrappers/shared/addEventHandlers.ts
+++ b/src/compile/render-dom/wrappers/shared/addEventHandlers.ts
@@ -7,7 +7,7 @@ export default function addEventHandlers(
 	handlers: EventHandler[]
 ) {
 	handlers.forEach(handler => {
-		let snippet = handler.render();
+		let snippet = handler.render(block);
 		if (handler.modifiers.has('preventDefault')) snippet = `@preventDefault(${snippet})`;
 		if (handler.modifiers.has('stopPropagation')) snippet = `@stopPropagation(${snippet})`;
 
@@ -26,11 +26,5 @@ export default function addEventHandlers(
 				`@addListener(${target}, "${handler.name}", ${snippet})`
 			);
 		}
-
-		if (handler.expression) {
-			handler.expression.declarations.forEach(declaration => {
-				block.builders.init.addBlock(declaration);
-			});
-		}
 	});
 }
\ No newline at end of file


Test patch:
diff --git a/test/runtime/samples/each-block-function/_config.js b/test/runtime/samples/each-block-function/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/each-block-function/_config.js
@@ -0,0 +1,15 @@
+export default {
+	html: `
+		<p>1, 2, 3</p>
+		<p>2, 4, 6</p>
+		<p>3, 6, 9</p>
+	`,
+
+	test({ assert, component, target }) {
+		component.numbers = [4, 5];
+		assert.htmlEqual( target.innerHTML, `
+			<p>16, 20</p>
+			<p>20, 25</p>
+		` );
+	}
+};
diff --git a/test/runtime/samples/each-block-function/main.html b/test/runtime/samples/each-block-function/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/each-block-function/main.html
@@ -0,0 +1,7 @@
+{#each numbers as i}
+	<p>{numbers.map(j => i * j).join(', ')}</p>
+{/each}
+
+<script>
+	export let numbers = [1, 2, 3];
+</script>