Repository: sveltejs/svelte (JavaScript)
Date: 2020-06-02 21:42:05+00:00
Task type: Bug Fix

Issue:
Assignment to const variable in inline event handler is ignored
**Describe the bug**
When an inline event handler assigns to a const variable no error is raised and the assigment is silently ignored.

**To Reproduce**
- [repl](https://svelte.dev/repl/2c8e583e379f49b3af2066f08ad5c2ba?version=3.22.3)
- minimal example:
```html
<script>
	const immutable = true
</script>
<button on:click={_ => immutable = false }>bug me!</button>
```

**Expected behavior**
A compile time error is raised.

**Information about your Svelte project:**
I have no project yet. I was just going thru the official tutorial where I noticed this.

**Severity**
For me, not annoying at all. Yet. .)

**Additional context**
Have a nice day!



Issue discussion:

Solution patch:
diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts
--- a/src/compiler/compile/Component.ts
+++ b/src/compiler/compile/Component.ts
@@ -788,6 +788,42 @@ export default class Component {
 					scope = map.get(node);
 				}
 
+				let deep = false;
+				let names: string[] | undefined; 
+
+				if (node.type === 'AssignmentExpression') {
+					deep = node.left.type === 'MemberExpression';
+					names = deep
+						? [get_object(node.left).name]
+						: extract_names(node.left);
+				} else if (node.type === 'UpdateExpression') {
+					deep = node.argument.type === 'MemberExpression';
+					const { name } = get_object(node.argument);
+					names = [name];
+				}
+
+				if (names) {
+					names.forEach(name => {
+						let current_scope = scope;
+						let declaration;
+
+						while (current_scope) {
+							if (current_scope.declarations.has(name)) {
+								declaration = current_scope.declarations.get(name);
+								break;
+							}
+							current_scope = current_scope.parent;
+						}
+
+						if (declaration && declaration.kind === 'const' && !deep) {
+							component.error(node as any, {
+								code: 'assignment-to-const',
+								message: 'You are assigning to a const'
+							});
+						}
+					});
+				}
+
 				if (node.type === 'ImportDeclaration') {
 					component.extract_imports(node);
 					// TODO: to use actual remove
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
@@ -126,6 +126,7 @@ export default class Expression {
 						deep = node.left.type === 'MemberExpression';
 						names = extract_names(deep ? get_object(node.left) : node.left);
 					} else if (node.type === 'UpdateExpression') {
+                        deep = node.argument.type === 'MemberExpression';
 						names = extract_names(get_object(node.argument));
 					}
 				}
@@ -147,7 +148,26 @@ export default class Expression {
 							component.add_reference(node, name);
 
 							const variable = component.var_lookup.get(name);
-							if (variable) variable[deep ? 'mutated' : 'reassigned'] = true;
+
+							if (variable) {
+								variable[deep ? 'mutated' : 'reassigned'] = true;
+							}
+
+							const declaration: any = scope.find_owner(name)?.declarations.get(name);
+
+							if (declaration) {
+								if (declaration.kind === 'const' && !deep) {
+									component.error(node, {
+										code: 'assignment-to-const',
+										message: 'You are assigning to a const'
+									});
+								}
+							} else if (variable && variable.writable === false && !deep) {
+								component.error(node, {
+									code: 'assignment-to-const',
+									message: 'You are assigning to a const'
+								});
+							}
 						}
 					});
 				}


Test patch:
diff --git a/test/js/samples/unchanged-expression/expected.js b/test/js/samples/unchanged-expression/expected.js
--- a/test/js/samples/unchanged-expression/expected.js
+++ b/test/js/samples/unchanged-expression/expected.js
@@ -73,7 +73,7 @@ let world1 = 'world';
 let world2 = 'world';
 
 function instance($$self, $$props, $$invalidate) {
-	const world3 = 'world';
+	let world3 = 'world';
 
 	function foo() {
 		$$invalidate(0, world3 = 'svelte');
diff --git a/test/js/samples/unchanged-expression/input.svelte b/test/js/samples/unchanged-expression/input.svelte
--- a/test/js/samples/unchanged-expression/input.svelte
+++ b/test/js/samples/unchanged-expression/input.svelte
@@ -1,7 +1,7 @@
 <script>
   let world1 = 'world';
   let world2 = 'world';
-  const world3 = 'world';
+  let world3 = 'world';
   function foo() {
     world3 = 'svelte';
   }
diff --git a/test/validator/samples/assignment-to-const-2/errors.json b/test/validator/samples/assignment-to-const-2/errors.json
new file mode 100644
--- /dev/null
+++ b/test/validator/samples/assignment-to-const-2/errors.json
@@ -0,0 +1,17 @@
+[
+	{
+		"code": "assignment-to-const",
+		"message": "You are assigning to a const",
+		"start": {
+			"line": 13,
+			"column": 24,
+			"character": 282
+		},
+		"end": {
+			"line": 13,
+			"column": 35,
+			"character": 293
+		},
+		"pos": 282
+	}
+]
\ No newline at end of file
diff --git a/test/validator/samples/assignment-to-const-2/input.svelte b/test/validator/samples/assignment-to-const-2/input.svelte
new file mode 100644
--- /dev/null
+++ b/test/validator/samples/assignment-to-const-2/input.svelte
@@ -0,0 +1,15 @@
+<script>
+	const immutable = 0;
+
+	const obj1 = { prop: true };
+	const obj2 = { prop: 0 }
+</script>
+
+<!-- should not error -->
+<button on:click={() => obj1.prop = false}>click</button>
+<button on:click={() => obj2.prop++}>click</button>
+
+<!-- should error -->
+<button on:click={() => immutable++}>click</button>
+
+
diff --git a/test/validator/samples/assignment-to-const-3/errors.json b/test/validator/samples/assignment-to-const-3/errors.json
new file mode 100644
--- /dev/null
+++ b/test/validator/samples/assignment-to-const-3/errors.json
@@ -0,0 +1,17 @@
+[
+	{
+		"code": "assignment-to-const",
+		"message": "You are assigning to a const",
+		"start": {
+			"line": 14,
+			"column": 3,
+			"character": 172
+		},
+		"end": {
+			"line": 14,
+			"column": 10,
+			"character": 179
+		},
+		"pos": 172
+	}
+]
\ No newline at end of file
diff --git a/test/validator/samples/assignment-to-const-3/input.svelte b/test/validator/samples/assignment-to-const-3/input.svelte
new file mode 100644
--- /dev/null
+++ b/test/validator/samples/assignment-to-const-3/input.svelte
@@ -0,0 +1,20 @@
+<script>
+	const foo = 'hello';
+	
+	function shouldNotError() {
+		let foo = 0;
+		
+		function inner() {
+			foo = 1;
+		}
+	}
+	
+	function shouldError() {
+		function inner() {
+			foo = 1;
+		}
+	}
+</script>
+
+<button on:click={shouldNotError}>click</button>
+<button on:click={shouldError}>click</button>
\ No newline at end of file
diff --git a/test/validator/samples/assignment-to-const-4/errors.json b/test/validator/samples/assignment-to-const-4/errors.json
new file mode 100644
--- /dev/null
+++ b/test/validator/samples/assignment-to-const-4/errors.json
@@ -0,0 +1,17 @@
+[
+	{
+		"code": "assignment-to-const",
+		"message": "You are assigning to a const",
+		"start": {
+			"line": 17,
+			"column": 2,
+			"character": 189
+		},
+		"end": {
+			"line": 17,
+			"column": 9,
+			"character": 196
+		},
+		"pos": 189
+	}
+]
\ No newline at end of file
diff --git a/test/validator/samples/assignment-to-const-4/input.svelte b/test/validator/samples/assignment-to-const-4/input.svelte
new file mode 100644
--- /dev/null
+++ b/test/validator/samples/assignment-to-const-4/input.svelte
@@ -0,0 +1,21 @@
+<script>
+	const foo = 'hello';
+</script>
+
+<button on:click={() => {
+	let foo = 0;
+		
+	function inner() {
+		foo = 1;
+	}
+}}>
+	click
+</button>
+
+<button on:click={() => {
+	function inner() {
+		foo = 1;
+	}
+}}>
+	click
+</button>
\ No newline at end of file
diff --git a/test/validator/samples/assignment-to-const/errors.json b/test/validator/samples/assignment-to-const/errors.json
new file mode 100644
--- /dev/null
+++ b/test/validator/samples/assignment-to-const/errors.json
@@ -0,0 +1,17 @@
+[
+	{
+		"code": "assignment-to-const",
+		"message": "You are assigning to a const",
+		"start": {
+			"line": 16,
+			"column": 2,
+			"character": 225
+		},
+		"end": {
+			"line": 16,
+			"column": 18,
+			"character": 241
+		},
+		"pos": 225
+	}
+]
\ No newline at end of file
diff --git a/test/validator/samples/assignment-to-const/input.svelte b/test/validator/samples/assignment-to-const/input.svelte
new file mode 100644
--- /dev/null
+++ b/test/validator/samples/assignment-to-const/input.svelte
@@ -0,0 +1,24 @@
+<script>
+	const immutable = false;
+
+	const obj1 = { prop: true };
+	const obj2 = { prop: 0 };
+
+	function shouldNotError() {
+		obj1.prop = false;
+	}
+
+	function shouldNotError2() {
+		obj2.prop++;
+	}
+
+	function shouldError() {
+		immutable = true
+	}
+
+	
+</script>
+
+<button on:click={shouldNotError}>click</button>
+<button on:click={shouldNotError2}>click</button>
+<button on:click={shouldError}>click</button>