Repository: sveltejs/svelte (JavaScript)
Date: 2020-02-08 16:40:48+00:00
Task type: Bug Fix

Issue:
Unable to deconstruct an object that has a reserved keyword
https://svelte.dev/repl/8ec7ec1e072b405d90549a5277a9f235?version=3.18.1


Issue discussion:
That's not possible in plain JavaScript either.
@pushkine `const { in: value } = { value: 12 }` is a syntax error in JS: https://jsfiddle.net/uakgp9ho/

So, it will be a syntax error in Svelte too.
@TehShrike your fiddle makes no sense
here it is corrected : https://jsfiddle.net/2n8efqh0/
jsfiddle does underline the keyword but the console doesn't output any error, hinting this is not a javascript issue but a linter one
I see no reason why one couldn't use a reserved keyword as a object property name
Hey, you're right
I believe we're currently using some custom parsing logic for the array/object destructuring in each blocks, and that probably doesn't realize that `in` can be used there in that way if it's trying to parse `in` by itself as an identifier. I'm not sure the reason we're not using Acorn to try to parse these (maybe because it wouldn't play nice with the parenthesized key afterwards?), but it seems like it would be nice if we could get that to happen.
Apparently all the way back to 3.0.0, the compiler has never balked at doing something like `{in}` or `{class}`, which it definitely should, as that results in invalid code: `text(in)` or `text(class)`.
Actually that's probably just a matter of removing the special handling added way back in #385.

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@
 * Fix binding to module-level variables ([#4086](https://github.com/sveltejs/svelte/issues/4086))
 * Disallow attribute/prop names from matching two-way-bound names or `{shorthand}` attribute/prop names ([#4325](https://github.com/sveltejs/svelte/issues/4325))
 * Improve performance of `flush()` by not using `.shift()` ([#4356](https://github.com/sveltejs/svelte/pull/4356))
+* Permit reserved keywords as destructuring keys in `{#each}` ([#4372](https://github.com/sveltejs/svelte/issues/4372))
+* Disallow reserved keywords in `{expressions}` ([#4372](https://github.com/sveltejs/svelte/issues/4372))
 * Fix code generation error with precedence of arrow functions ([#4384](https://github.com/sveltejs/svelte/issues/4384))
 
 ## 3.18.1
diff --git a/src/compiler/parse/index.ts b/src/compiler/parse/index.ts
--- a/src/compiler/parse/index.ts
+++ b/src/compiler/parse/index.ts
@@ -141,7 +141,7 @@ export class Parser {
 		return result;
 	}
 
-	read_identifier() {
+	read_identifier(allow_reserved = false) {
 		const start = this.index;
 
 		let i = this.index;
@@ -160,7 +160,7 @@ export class Parser {
 
 		const identifier = this.template.slice(this.index, this.index = i);
 
-		if (reserved.has(identifier)) {
+		if (!allow_reserved && reserved.has(identifier)) {
 			this.error({
 				code: `unexpected-reserved-word`,
 				message: `'${identifier}' is a reserved word in JavaScript and cannot be used here`
diff --git a/src/compiler/parse/read/context.ts b/src/compiler/parse/read/context.ts
--- a/src/compiler/parse/read/context.ts
+++ b/src/compiler/parse/read/context.ts
@@ -1,4 +1,5 @@
 import { Parser } from '../index';
+import { reserved } from '../../utils/names';
 
 interface Identifier {
 	start: number;
@@ -116,8 +117,11 @@ export default function read_context(parser: Parser) {
 				break;
 			}
 
+			// TODO: DRY this out somehow
+			// We don't know whether we want to allow reserved words until we see whether there's a ':' after it
+			// Probably ideally we'd use Acorn to do all of this
 			const start = parser.index;
-			const name = parser.read_identifier();
+			const name = parser.read_identifier(true);
 			const key: Identifier = {
 				start,
 				end: parser.index,
@@ -126,9 +130,19 @@ export default function read_context(parser: Parser) {
 			};
 			parser.allow_whitespace();
 
-			const value = parser.eat(':')
-				? (parser.allow_whitespace(), read_context(parser))
-				: key;
+			let value: Context;
+			if (parser.eat(':')) {
+				parser.allow_whitespace();
+				value = read_context(parser);
+			} else {
+				if (reserved.has(name)) {
+					parser.error({
+						code: `unexpected-reserved-word`,
+						message: `'${name}' is a reserved word in JavaScript and cannot be used here`
+					}, start);
+				}
+				value = key;
+			}
 
 			const property: Property = {
 				start,
diff --git a/src/compiler/parse/read/expression.ts b/src/compiler/parse/read/expression.ts
--- a/src/compiler/parse/read/expression.ts
+++ b/src/compiler/parse/read/expression.ts
@@ -1,37 +1,9 @@
 import { parse_expression_at } from '../acorn';
 import { Parser } from '../index';
-import { Identifier, Node, SimpleLiteral } from 'estree';
+import { Node } from 'estree';
 import { whitespace } from '../../utils/patterns';
 
-const literals = new Map([['true', true], ['false', false], ['null', null]]);
-
 export default function read_expression(parser: Parser): Node {
-	const start = parser.index;
-
-	const name = parser.read_until(/\s*}/);
-	if (name && /^[a-z]+$/.test(name)) {
-		const end = start + name.length;
-
-		if (literals.has(name)) {
-			return {
-				type: 'Literal',
-				start,
-				end,
-				value: literals.get(name),
-				raw: name,
-			} as SimpleLiteral;
-		}
-
-		return {
-			type: 'Identifier',
-			start,
-			end: start + name.length,
-			name,
-		} as Identifier;
-	}
-
-	parser.index = start;
-
 	try {
 		const node = parse_expression_at(parser.template, parser.index);
 


Test patch:
diff --git a/test/parser/samples/action-with-identifier/output.json b/test/parser/samples/action-with-identifier/output.json
--- a/test/parser/samples/action-with-identifier/output.json
+++ b/test/parser/samples/action-with-identifier/output.json
@@ -20,6 +20,16 @@
 							"type": "Identifier",
 							"start": 20,
 							"end": 27,
+							"loc": {
+								"start": {
+									"line": 1,
+									"column": 20
+								},
+								"end": {
+									"line": 1,
+									"column": 27
+								}
+							},
 							"name": "message"
 						}
 					}
diff --git a/test/parser/samples/attribute-dynamic-boolean/output.json b/test/parser/samples/attribute-dynamic-boolean/output.json
--- a/test/parser/samples/attribute-dynamic-boolean/output.json
+++ b/test/parser/samples/attribute-dynamic-boolean/output.json
@@ -24,6 +24,16 @@
 									"type": "Identifier",
 									"start": 20,
 									"end": 28,
+									"loc": {
+										"start": {
+											"line": 1,
+											"column": 20
+										},
+										"end": {
+											"line": 1,
+											"column": 28
+										}
+									},
 									"name": "readonly"
 								}
 							}
diff --git a/test/parser/samples/attribute-dynamic-reserved/input.svelte b/test/parser/samples/attribute-dynamic-reserved/input.svelte
deleted file mode 100644
--- a/test/parser/samples/attribute-dynamic-reserved/input.svelte
+++ /dev/null
@@ -1 +0,0 @@
-<div class={class}></div>
\ No newline at end of file
diff --git a/test/parser/samples/attribute-dynamic-reserved/output.json b/test/parser/samples/attribute-dynamic-reserved/output.json
deleted file mode 100644
--- a/test/parser/samples/attribute-dynamic-reserved/output.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
-	"html": {
-		"start": 0,
-		"end": 25,
-		"type": "Fragment",
-		"children": [
-			{
-				"start": 0,
-				"end": 25,
-				"type": "Element",
-				"name": "div",
-				"attributes": [
-					{
-						"start": 5,
-						"end": 18,
-						"type": "Attribute",
-						"name": "class",
-						"value": [
-							{
-								"start": 11,
-								"end": 18,
-								"type": "MustacheTag",
-								"expression": {
-									"type": "Identifier",
-									"start": 12,
-									"end": 17,
-									"name": "class"
-								}
-							}
-						]
-					}
-				],
-				"children": []
-			}
-		]
-	}
-}
\ No newline at end of file
diff --git a/test/parser/samples/attribute-dynamic/output.json b/test/parser/samples/attribute-dynamic/output.json
--- a/test/parser/samples/attribute-dynamic/output.json
+++ b/test/parser/samples/attribute-dynamic/output.json
@@ -31,6 +31,16 @@
 									"type": "Identifier",
 									"start": 20,
 									"end": 25,
+									"loc": {
+										"start": {
+											"line": 1,
+											"column": 20
+										},
+										"end": {
+											"line": 1,
+											"column": 25
+										}
+									},
 									"name": "color"
 								}
 							},
@@ -53,6 +63,16 @@
 							"type": "Identifier",
 							"start": 30,
 							"end": 35,
+							"loc": {
+								"start": {
+									"line": 1,
+									"column": 30
+								},
+								"end": {
+									"line": 1,
+									"column": 35
+								}
+							},
 							"name": "color"
 						}
 					}
diff --git a/test/parser/samples/attribute-with-whitespace/output.json b/test/parser/samples/attribute-with-whitespace/output.json
--- a/test/parser/samples/attribute-with-whitespace/output.json
+++ b/test/parser/samples/attribute-with-whitespace/output.json
@@ -20,6 +20,16 @@
 							"type": "Identifier",
 							"start": 19,
 							"end": 22,
+							"loc": {
+								"start": {
+									"line": 1,
+									"column": 19
+								},
+								"end": {
+									"line": 1,
+									"column": 22
+								}
+							},
 							"name": "foo"
 						}
 					}
diff --git a/test/parser/samples/binding/output.json b/test/parser/samples/binding/output.json
--- a/test/parser/samples/binding/output.json
+++ b/test/parser/samples/binding/output.json
@@ -27,6 +27,16 @@
 							"type": "Identifier",
 							"start": 50,
 							"end": 54,
+							"loc": {
+								"start": {
+									"line": 5,
+									"column": 19
+								},
+								"end": {
+									"line": 5,
+									"column": 23
+								}
+							},
 							"name": "name"
 						}
 					}
diff --git a/test/parser/samples/each-block-destructured/output.json b/test/parser/samples/each-block-destructured/output.json
--- a/test/parser/samples/each-block-destructured/output.json
+++ b/test/parser/samples/each-block-destructured/output.json
@@ -40,6 +40,16 @@
 									"type": "Identifier",
 									"start": 46,
 									"end": 49,
+									"loc": {
+										"start": {
+											"line": 2,
+											"column": 5
+										},
+										"end": {
+											"line": 2,
+											"column": 8
+										}
+									},
 									"name": "key"
 								}
 							},
@@ -58,6 +68,16 @@
 									"type": "Identifier",
 									"start": 53,
 									"end": 58,
+									"loc": {
+										"start": {
+											"line": 2,
+											"column": 12
+										},
+										"end": {
+											"line": 2,
+											"column": 17
+										}
+									},
 									"name": "value"
 								}
 							}
diff --git a/test/parser/samples/each-block-else/output.json b/test/parser/samples/each-block-else/output.json
--- a/test/parser/samples/each-block-else/output.json
+++ b/test/parser/samples/each-block-else/output.json
@@ -40,6 +40,16 @@
 									"type": "Identifier",
 									"start": 31,
 									"end": 37,
+									"loc": {
+										"start": {
+											"line": 2,
+											"column": 5
+										},
+										"end": {
+											"line": 2,
+											"column": 11
+										}
+									},
 									"name": "animal"
 								}
 							}
diff --git a/test/parser/samples/each-block-indexed/output.json b/test/parser/samples/each-block-indexed/output.json
--- a/test/parser/samples/each-block-indexed/output.json
+++ b/test/parser/samples/each-block-indexed/output.json
@@ -40,6 +40,16 @@
 									"type": "Identifier",
 									"start": 34,
 									"end": 35,
+									"loc": {
+										"start": {
+											"line": 2,
+											"column": 5
+										},
+										"end": {
+											"line": 2,
+											"column": 6
+										}
+									},
 									"name": "i"
 								}
 							},
@@ -58,6 +68,16 @@
 									"type": "Identifier",
 									"start": 39,
 									"end": 45,
+									"loc": {
+										"start": {
+											"line": 2,
+											"column": 10
+										},
+										"end": {
+											"line": 2,
+											"column": 16
+										}
+									},
 									"name": "animal"
 								}
 							}
diff --git a/test/parser/samples/each-block-keyed/output.json b/test/parser/samples/each-block-keyed/output.json
--- a/test/parser/samples/each-block-keyed/output.json
+++ b/test/parser/samples/each-block-keyed/output.json
@@ -40,6 +40,16 @@
 									"type": "Identifier",
 									"start": 37,
 									"end": 41,
+									"loc": {
+										"start": {
+											"line": 2,
+											"column": 5
+										},
+										"end": {
+											"line": 2,
+											"column": 9
+										}
+									},
 									"name": "todo"
 								}
 							}
diff --git a/test/parser/samples/each-block/output.json b/test/parser/samples/each-block/output.json
--- a/test/parser/samples/each-block/output.json
+++ b/test/parser/samples/each-block/output.json
@@ -40,6 +40,16 @@
 									"type": "Identifier",
 									"start": 31,
 									"end": 37,
+									"loc": {
+										"start": {
+											"line": 2,
+											"column": 5
+										},
+										"end": {
+											"line": 2,
+											"column": 11
+										}
+									},
 									"name": "animal"
 								}
 							}
diff --git a/test/parser/samples/element-with-mustache/output.json b/test/parser/samples/element-with-mustache/output.json
--- a/test/parser/samples/element-with-mustache/output.json
+++ b/test/parser/samples/element-with-mustache/output.json
@@ -26,6 +26,16 @@
 							"type": "Identifier",
 							"start": 11,
 							"end": 15,
+							"loc": {
+								"start": {
+									"line": 1,
+									"column": 11
+								},
+								"end": {
+									"line": 1,
+									"column": 15
+								}
+							},
 							"name": "name"
 						}
 					},
diff --git a/test/parser/samples/event-handler/output.json b/test/parser/samples/event-handler/output.json
--- a/test/parser/samples/event-handler/output.json
+++ b/test/parser/samples/event-handler/output.json
@@ -128,6 +128,16 @@
 					"type": "Identifier",
 					"start": 68,
 					"end": 75,
+					"loc": {
+						"start": {
+							"line": 3,
+							"column": 5
+						},
+						"end": {
+							"line": 3,
+							"column": 12
+						}
+					},
 					"name": "visible"
 				},
 				"children": [
diff --git a/test/parser/samples/if-block-else/output.json b/test/parser/samples/if-block-else/output.json
--- a/test/parser/samples/if-block-else/output.json
+++ b/test/parser/samples/if-block-else/output.json
@@ -12,6 +12,16 @@
 					"type": "Identifier",
 					"start": 5,
 					"end": 8,
+					"loc": {
+						"start": {
+							"line": 1,
+							"column": 5
+						},
+						"end": {
+							"line": 1,
+							"column": 8
+						}
+					},
 					"name": "foo"
 				},
 				"children": [
diff --git a/test/parser/samples/if-block/output.json b/test/parser/samples/if-block/output.json
--- a/test/parser/samples/if-block/output.json
+++ b/test/parser/samples/if-block/output.json
@@ -12,6 +12,16 @@
 					"type": "Identifier",
 					"start": 5,
 					"end": 8,
+					"loc": {
+						"start": {
+							"line": 1,
+							"column": 5
+						},
+						"end": {
+							"line": 1,
+							"column": 8
+						}
+					},
 					"name": "foo"
 				},
 				"children": [
diff --git a/test/parser/samples/implicitly-closed-li-block/output.json b/test/parser/samples/implicitly-closed-li-block/output.json
--- a/test/parser/samples/implicitly-closed-li-block/output.json
+++ b/test/parser/samples/implicitly-closed-li-block/output.json
@@ -40,6 +40,16 @@
 									"type": "Literal",
 									"start": 18,
 									"end": 22,
+									"loc": {
+										"start": {
+											"line": 3,
+											"column": 6
+										},
+										"end": {
+											"line": 3,
+											"column": 10
+										}
+									},
 									"value": true,
 									"raw": "true"
 								},
diff --git a/test/parser/samples/refs/output.json b/test/parser/samples/refs/output.json
--- a/test/parser/samples/refs/output.json
+++ b/test/parser/samples/refs/output.json
@@ -27,6 +27,16 @@
 							"type": "Identifier",
 							"start": 49,
 							"end": 52,
+							"loc": {
+								"start": {
+									"line": 5,
+									"column": 19
+								},
+								"end": {
+									"line": 5,
+									"column": 22
+								}
+							},
 							"name": "foo"
 						}
 					}
diff --git a/test/parser/samples/script-comment-trailing-multiline/output.json b/test/parser/samples/script-comment-trailing-multiline/output.json
--- a/test/parser/samples/script-comment-trailing-multiline/output.json
+++ b/test/parser/samples/script-comment-trailing-multiline/output.json
@@ -33,6 +33,16 @@
 							"type": "Identifier",
 							"start": 90,
 							"end": 94,
+							"loc": {
+								"start": {
+									"line": 9,
+									"column": 11
+								},
+								"end": {
+									"line": 9,
+									"column": 15
+								}
+							},
 							"name": "name"
 						}
 					},
diff --git a/test/parser/samples/script-comment-trailing/output.json b/test/parser/samples/script-comment-trailing/output.json
--- a/test/parser/samples/script-comment-trailing/output.json
+++ b/test/parser/samples/script-comment-trailing/output.json
@@ -33,6 +33,16 @@
 							"type": "Identifier",
 							"start": 79,
 							"end": 83,
+							"loc": {
+								"start": {
+									"line": 7,
+									"column": 11
+								},
+								"end": {
+									"line": 7,
+									"column": 15
+								}
+							},
 							"name": "name"
 						}
 					},
diff --git a/test/parser/samples/script/output.json b/test/parser/samples/script/output.json
--- a/test/parser/samples/script/output.json
+++ b/test/parser/samples/script/output.json
@@ -33,6 +33,16 @@
 							"type": "Identifier",
 							"start": 52,
 							"end": 56,
+							"loc": {
+								"start": {
+									"line": 5,
+									"column": 11
+								},
+								"end": {
+									"line": 5,
+									"column": 15
+								}
+							},
 							"name": "name"
 						}
 					},
diff --git a/test/parser/samples/space-between-mustaches/output.json b/test/parser/samples/space-between-mustaches/output.json
--- a/test/parser/samples/space-between-mustaches/output.json
+++ b/test/parser/samples/space-between-mustaches/output.json
@@ -26,6 +26,16 @@
 							"type": "Identifier",
 							"start": 5,
 							"end": 6,
+							"loc": {
+								"start": {
+									"line": 1,
+									"column": 5
+								},
+								"end": {
+									"line": 1,
+									"column": 6
+								}
+							},
 							"name": "a"
 						}
 					},
@@ -44,6 +54,16 @@
 							"type": "Identifier",
 							"start": 9,
 							"end": 10,
+							"loc": {
+								"start": {
+									"line": 1,
+									"column": 9
+								},
+								"end": {
+									"line": 1,
+									"column": 10
+								}
+							},
 							"name": "b"
 						}
 					},
@@ -62,6 +82,16 @@
 							"type": "Identifier",
 							"start": 15,
 							"end": 16,
+							"loc": {
+								"start": {
+									"line": 1,
+									"column": 15
+								},
+								"end": {
+									"line": 1,
+									"column": 16
+								}
+							},
 							"name": "c"
 						}
 					},
diff --git a/test/parser/samples/spread/output.json b/test/parser/samples/spread/output.json
--- a/test/parser/samples/spread/output.json
+++ b/test/parser/samples/spread/output.json
@@ -18,6 +18,16 @@
 							"type": "Identifier",
 							"start": 9,
 							"end": 14,
+							"loc": {
+								"start": {
+									"line": 1,
+									"column": 9
+								},
+								"end": {
+									"line": 1,
+									"column": 14
+								}
+							},
 							"name": "props"
 						}
 					}
diff --git a/test/parser/samples/textarea-children/output.json b/test/parser/samples/textarea-children/output.json
--- a/test/parser/samples/textarea-children/output.json
+++ b/test/parser/samples/textarea-children/output.json
@@ -29,6 +29,16 @@
 									"type": "Identifier",
 									"start": 41,
 									"end": 44,
+									"loc": {
+										"start": {
+											"line": 2,
+											"column": 30
+										},
+										"end": {
+											"line": 2,
+											"column": 33
+										}
+									},
 									"name": "foo"
 								}
 							},
diff --git a/test/parser/samples/whitespace-normal/output.json b/test/parser/samples/whitespace-normal/output.json
--- a/test/parser/samples/whitespace-normal/output.json
+++ b/test/parser/samples/whitespace-normal/output.json
@@ -33,6 +33,16 @@
 									"type": "Identifier",
 									"start": 19,
 									"end": 23,
+									"loc": {
+										"start": {
+											"line": 1,
+											"column": 19
+										},
+										"end": {
+											"line": 1,
+											"column": 23
+										}
+									},
 									"name": "name"
 								}
 							},
diff --git a/test/parser/samples/yield/input.svelte b/test/parser/samples/yield/input.svelte
deleted file mode 100644
--- a/test/parser/samples/yield/input.svelte
+++ /dev/null
@@ -1 +0,0 @@
-{yield}
\ No newline at end of file
diff --git a/test/parser/samples/yield/output.json b/test/parser/samples/yield/output.json
deleted file mode 100644
--- a/test/parser/samples/yield/output.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
-	"html": {
-		"start": 0,
-		"end": 7,
-		"type": "Fragment",
-		"children": [
-			{
-				"start": 0,
-				"end": 7,
-				"type": "MustacheTag",
-				"expression": {
-					"type": "Identifier",
-					"start": 1,
-					"end": 6,
-					"name": "yield"
-				}
-			}
-		]
-	}
-}
\ No newline at end of file
diff --git a/test/runtime/samples/each-block-destructured-object-reserved-key/_config.js b/test/runtime/samples/each-block-destructured-object-reserved-key/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/each-block-destructured-object-reserved-key/_config.js
@@ -0,0 +1,5 @@
+export default {
+	html: `
+		<p>bar</p>
+	`
+};
diff --git a/test/runtime/samples/each-block-destructured-object-reserved-key/main.svelte b/test/runtime/samples/each-block-destructured-object-reserved-key/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/each-block-destructured-object-reserved-key/main.svelte
@@ -0,0 +1,7 @@
+<script>
+	const foo = [{ in: 'bar' }];
+</script>
+
+{#each foo as { in: bar }}
+	<p>{bar}</p>
+{/each}
diff --git a/test/validator/samples/each-block-invalid-context-destructured-object/errors.json b/test/validator/samples/each-block-invalid-context-destructured-object/errors.json
new file mode 100644
--- /dev/null
+++ b/test/validator/samples/each-block-invalid-context-destructured-object/errors.json
@@ -0,0 +1,15 @@
+[{
+	"code": "unexpected-reserved-word",
+	"message": "'case' is a reserved word in JavaScript and cannot be used here",
+	"start": {
+		"line": 1,
+		"column": 18,
+		"character": 18
+	},
+	"end": {
+		"line": 1,
+		"column": 18,
+		"character": 18
+	},
+	"pos": 18
+}]
diff --git a/test/validator/samples/each-block-invalid-context-destructured-object/input.svelte b/test/validator/samples/each-block-invalid-context-destructured-object/input.svelte
new file mode 100644
--- /dev/null
+++ b/test/validator/samples/each-block-invalid-context-destructured-object/input.svelte
@@ -0,0 +1,3 @@
+{#each cases as { case }}
+	{case.title}
+{/each}