Repository: sveltejs/svelte (JavaScript)
Task type: Bug Fix

Issue:
Element name can conflict with local context var
[REPL](https://svelte.technology/repl?version=1.57.4&gist=67af4efd3216f0ddadf3c66e8b7c5e05) — the `i` in `{{#each tagList as tag, i}}` conflicts with the `<i>` element
Element name can conflict with local context var
[REPL](https://svelte.technology/repl?version=1.57.4&gist=67af4efd3216f0ddadf3c66e8b7c5e05) — the `i` in `{{#each tagList as tag, i}}` conflicts with the `<i>` element


Discussion:

Solution patch:
diff --git a/src/generators/nodes/EachBlock.ts b/src/generators/nodes/EachBlock.ts
--- a/src/generators/nodes/EachBlock.ts
+++ b/src/generators/nodes/EachBlock.ts
@@ -54,9 +54,11 @@ export default class EachBlock extends Node {
 		this.block.contextTypes.set(this.context, 'each');
 		this.block.indexNames.set(this.context, indexName);
 		this.block.listNames.set(this.context, listName);
+
 		if (this.index) {
+			this.block.getUniqueName(this.index); // this prevents name collisions (#1254)
 			this.block.indexes.set(this.index, this.context);
-			this.block.changeableIndexes.set(this.index, this.key)
+			this.block.changeableIndexes.set(this.index, this.key); // TODO is this right?
 		}
 
 		const context = this.block.getUniqueName(this.context);


Test patch:
diff --git a/test/helpers.js b/test/helpers.js
--- a/test/helpers.js
+++ b/test/helpers.js
@@ -174,7 +174,7 @@ function capitalise(str) {
 	return str[0].toUpperCase() + str.slice(1);
 }
 
-export function showOutput(cwd, options = {}, s = svelte) {
+export function showOutput(cwd, options = {}, compile = svelte.compile) {
 	glob.sync('**/*.html', { cwd }).forEach(file => {
 		if (file[0] === '_') return;
 
@@ -183,7 +183,7 @@ export function showOutput(cwd, options = {}, s = svelte) {
 			.replace(/^\d/, '_$&')
 			.replace(/[^a-zA-Z0-9_$]/g, '');
 
-		const { code } = s.compile(
+		const { code } = compile(
 			fs.readFileSync(`${cwd}/${file}`, 'utf-8'),
 			Object.assign(options, {
 				filename: file,
diff --git a/test/runtime/index.js b/test/runtime/index.js
--- a/test/runtime/index.js
+++ b/test/runtime/index.js
@@ -14,9 +14,11 @@ import {
 	spaces
 } from "../helpers.js";
 
+let svelte$;
 let svelte;
 
 let compileOptions = null;
+let compile = null;
 
 function getName(filename) {
 	const base = path.basename(filename).replace(".html", "");
@@ -25,7 +27,8 @@ function getName(filename) {
 
 describe("runtime", () => {
 	before(() => {
-		svelte = loadSvelte(true);
+		svelte = loadSvelte(false);
+		svelte$ = loadSvelte(true);
 
 		require.extensions[".html"] = function(module, filename) {
 			const options = Object.assign(
@@ -33,7 +36,7 @@ describe("runtime", () => {
 				compileOptions
 			);
 
-			const { code } = svelte.compile(fs.readFileSync(filename, "utf-8"), options);
+			const { code } = compile(fs.readFileSync(filename, "utf-8"), options);
 
 			return module._compile(code, filename);
 		};
@@ -58,6 +61,8 @@ describe("runtime", () => {
 				throw new Error('skipping test, already failed');
 			}
 
+			compile = (config.preserveIdentifiers ? svelte : svelte$).compile;
+
 			const cwd = path.resolve(`test/runtime/samples/${dir}`);
 			global.document.title = '';
 
@@ -75,7 +80,7 @@ describe("runtime", () => {
 						`test/runtime/samples/${dir}/main.html`,
 						"utf-8"
 					);
-					const { code } = svelte.compile(source, compileOptions);
+					const { code } = compile(source, compileOptions);
 					const startIndex = code.indexOf("function create_main_fragment"); // may change!
 					if (startIndex === -1) throw new Error("missing create_main_fragment");
 					const endIndex = code.lastIndexOf("export default");
@@ -95,7 +100,7 @@ describe("runtime", () => {
 					if (err.frame) {
 						console.error(chalk.red(err.frame)); // eslint-disable-line no-console
 					}
-					showOutput(cwd, { shared, format: 'cjs', store: !!compileOptions.store }, svelte); // eslint-disable-line no-console
+					showOutput(cwd, { shared, format: 'cjs', store: !!compileOptions.store }, compile); // eslint-disable-line no-console
 					throw err;
 				}
 			}
@@ -140,7 +145,7 @@ describe("runtime", () => {
 					try {
 						SvelteComponent = require(`./samples/${dir}/main.html`);
 					} catch (err) {
-						showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate, store: !!compileOptions.store }, svelte); // eslint-disable-line no-console
+						showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate, store: !!compileOptions.store }, compile); // eslint-disable-line no-console
 						throw err;
 					}
 
@@ -198,12 +203,12 @@ describe("runtime", () => {
 						config.error(assert, err);
 					} else {
 						failed.add(dir);
-						showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate, store: !!compileOptions.store }, svelte); // eslint-disable-line no-console
+						showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate, store: !!compileOptions.store }, compile); // eslint-disable-line no-console
 						throw err;
 					}
 				})
 				.then(() => {
-					if (config.show) showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate, store: !!compileOptions.store }, svelte);
+					if (config.show) showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate, store: !!compileOptions.store }, compile);
 				});
 		});
 	}
@@ -216,7 +221,7 @@ describe("runtime", () => {
 	});
 
 	it("fails if options.target is missing in dev mode", () => {
-		const { code } = svelte.compile(`<div></div>`, {
+		const { code } = svelte$.compile(`<div></div>`, {
 			format: "iife",
 			name: "SvelteComponent",
 			dev: true
@@ -232,7 +237,7 @@ describe("runtime", () => {
 	});
 
 	it("fails if options.hydrate is true but the component is non-hydratable", () => {
-		const { code } = svelte.compile(`<div></div>`, {
+		const { code } = svelte$.compile(`<div></div>`, {
 			format: "iife",
 			name: "SvelteComponent",
 			dev: true
diff --git a/test/runtime/samples/deconflict-elements-indexes/_config.js b/test/runtime/samples/deconflict-elements-indexes/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/deconflict-elements-indexes/_config.js
@@ -0,0 +1,22 @@
+export default {
+	html: `
+		<div>
+			<i>one</i>
+		</div>
+	`,
+
+	preserveIdentifiers: true,
+
+	test(assert, component, target) {
+		const { tagList } = component.get();
+		tagList.push('two');
+		component.set({ tagList });
+
+		assert.htmlEqual(target.innerHTML, `
+			<div>
+				<i>one</i>
+				<i>two</i>
+			</div>
+		`);
+	}
+};
\ No newline at end of file
diff --git a/test/runtime/samples/deconflict-elements-indexes/main.html b/test/runtime/samples/deconflict-elements-indexes/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/deconflict-elements-indexes/main.html
@@ -0,0 +1,24 @@
+<div>
+	{{#each tagList as tag, i}}
+		<i on:click='remove(i)'>
+			{{tag}}
+		</i>
+	{{/each}}
+</div>
+
+<script>
+	export default {
+		data() {
+			return {
+				inProgress: false,
+				tagList: ['one']
+			};
+		},
+
+		methods: {
+			remove(index) {
+				// ...
+			}
+		}
+	};
+</script>
\ No newline at end of file