Repository: sveltejs/svelte (JavaScript)
Date: 2018-04-15 18:51:50+00:00
Task type: Feature

Issue:
Destructuring syntax for computed properties
I was about to respond to [this HN comment](https://news.ycombinator.com/item?id=16056010) explaining why it'd be impossible, but then I realised... it isn't!

```js
computed: {
  hours: ({time}) => time.getHours(),
  minutes: ({time}) => time.getMinutes(),
  seconds: ({time}) => time.getSeconds()
}
```

As long as you do in fact use destructuring (i.e. not `state => state.time.getHours()`), Svelte could get all the information it needs to compile these computed properties. As a bonus, you could do things like this:

```js
computed: {
  foo: ({bar: baz = qux}) => baz.toUpperCase()
}
```

(Aggressive destructuring results in unreadable garbage code, but hey, it's JS!)

It would be a breaking change, I guess (since we'd need to error on non-destructured arguments), but *maybe* it's a good one? Genuinely unsure. Thoughts welcome.


Issue discussion:
That would be a trivial-but-annoying change to make across our codebase. What's the benefit?

I don't even understand what the second version is doing, so maybe that's why I don't get the value proposition yet (beyond typing more characters).
I don't think it makes the code more readable and while it could give more options for destructing, I don't think it's worth a breaking change even for a major version.
I voted thumbs down on this under the assumption that the old syntax would be removed. It doesn't seem obvious though that that would necessarily have to be the case. Couldn't both syntaxes be supported without resulting in ambiguity?

Even if both syntaxes were supported, I still don't see a lot of benefit to this. I think the existing syntax is perfectly fine.
Isn't it self-evident that it's preferable to stick to the standard language semantics than make up your own thing, unless the discrepancy affords some major benefit? Repurposing syntax like this is just _weird_. It's completely unexpected that the name of an argument affects what the function does, or that a particular function has to be a literal. I would also argue that it goes against the spirit of Svelte, from the guide:

> Rather than reinventing the wheel, Svelte templates are built on foundations that have stood the test of time: HTML, CSS and JavaScript.

I think the discussion in this thread shows exactly why it is so problematic and urgent to fix, so not more Svelte users paint themselves into this corner. There's no benefit to having this odd thing around, and it's not even central to what Svelte is all about, but the longer it sticks around, the more painful it will be to remove it.

Here are some ideas for how to ease the transition:

- Add a `--legacy` compiler flag that continues to parse argument names as dependencies
- Create a codemod to automatically rewrite every existing computed property in the codebase
The breaking change might be annoying, but this does seem like the better way to go. :+1: 

This would enable what I was [talking about in chat](https://gitter.im/sveltejs/svelte?at=5ac652c827c509a774df1edc) a little while back (making the spread operator fully workable), and remove any need for #1303.
I realised I promoted this in #1338 but didn't say anything in here, so:

I'm totally convinced that this is the right move. It solves real problems, and is less *weird*. The current dependency injection is something people often remark upon; it really grosses people out.

We can automatically upgrade components with [svelte-upgrade](https://github.com/sveltejs/svelte-upgrade).
Ha, I just noticed that me and @trbrc both used the word *weird* in italics. Nuff said!

Solution patch:
diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts
--- a/src/generators/Generator.ts
+++ b/src/generators/Generator.ts
@@ -84,6 +84,7 @@ export default class Generator {
 	source: string;
 	name: string;
 	options: CompileOptions;
+	v2: boolean;
 
 	customElement: CustomElementOptions;
 	tag: string;
@@ -132,6 +133,8 @@ export default class Generator {
 		stats.start('compile');
 		this.stats = stats;
 
+		this.v2 = options.parser === 'v2';
+
 		this.ast = clone(parsed);
 
 		this.parsed = parsed;
@@ -575,10 +578,10 @@ export default class Generator {
 						const key = getName(prop.key);
 						const value = prop.value;
 
-						const deps = value.params.map(
-							(param: Node) =>
-								param.type === 'AssignmentPattern' ? param.left.name : param.name
-						);
+						const deps = this.v2
+							? value.params[0].properties.map(prop => prop.key.name)
+							: value.params.map(param => param.type === 'AssignmentPattern' ? param.left.name : param.name);
+
 						deps.forEach(dep => {
 							this.expectedProperties.add(dep);
 						});
diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts
--- a/src/generators/dom/index.ts
+++ b/src/generators/dom/index.ts
@@ -99,9 +99,11 @@ export default function dom(
 
 			const condition = `${deps.map(dep => `changed.${dep}`).join(' || ')}`;
 
-			const statement = `if (this._differs(state.${key}, (state.${key} = %computed-${key}(${deps
-				.map(dep => `state.${dep}`)
-				.join(', ')})))) changed.${key} = true;`;
+			const call = generator.v2
+				? `%computed-${key}(state)`
+				: `%computed-${key}(${deps.map(dep => `state.${dep}`).join(', ')})`;
+
+			const statement = `if (this._differs(state.${key}, (state.${key} = ${call}))) changed.${key} = true;`;
 
 			computationBuilder.addConditional(condition, statement);
 		});
diff --git a/src/validate/index.ts b/src/validate/index.ts
--- a/src/validate/index.ts
+++ b/src/validate/index.ts
@@ -10,6 +10,7 @@ import { Node, Parsed, CompileOptions, Warning } from '../interfaces';
 export class Validator {
 	readonly source: string;
 	readonly filename: string;
+	readonly v2: boolean;
 
 	options: CompileOptions;
 	onwarn: ({}) => void;
@@ -38,6 +39,7 @@ export class Validator {
 		this.filename = options.filename;
 		this.onwarn = options.onwarn;
 		this.options = options;
+		this.v2 = options.parser === 'v2';
 
 		this.namespace = null;
 		this.defaultExport = null;
@@ -96,7 +98,7 @@ export default function validate(
 	stylesheet: Stylesheet,
 	options: CompileOptions
 ) {
-	const { onwarn, onerror, name, filename, store, dev } = options;
+	const { onwarn, onerror, name, filename, store, dev, parser } = options;
 
 	try {
 		if (name && !/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(name)) {
@@ -119,7 +121,8 @@ export default function validate(
 			name,
 			filename,
 			store,
-			dev
+			dev,
+			parser
 		});
 
 		if (parsed.js) {
diff --git a/src/validate/js/propValidators/computed.ts b/src/validate/js/propValidators/computed.ts
--- a/src/validate/js/propValidators/computed.ts
+++ b/src/validate/js/propValidators/computed.ts
@@ -7,6 +7,7 @@ import { Validator } from '../../index';
 import { Node } from '../../../interfaces';
 import walkThroughTopFunctionScope from '../../../utils/walkThroughTopFunctionScope';
 import isThisGetCallExpression from '../../../utils/isThisGetCallExpression';
+import validCalleeObjects from '../../../utils/validCalleeObjects';
 
 const isFunctionExpression = new Set([
 	'FunctionExpression',
@@ -74,19 +75,37 @@ export default function computed(validator: Validator, prop: Node) {
 			});
 		}
 
-		params.forEach((param: Node) => {
-			const valid =
-				param.type === 'Identifier' ||
-				(param.type === 'AssignmentPattern' &&
-					param.left.type === 'Identifier');
-
-			if (!valid) {
-				// TODO change this for v2
-				validator.error(param, {
+		if (validator.v2) {
+			if (params.length > 1) {
+				validator.error(computation.value, {
 					code: `invalid-computed-arguments`,
-					message: `Computed properties cannot use destructuring in function parameters`
+					message: `Computed properties must take a single argument`
 				});
 			}
-		});
+
+			const param = params[0];
+			if (param.type !== 'ObjectPattern') {
+				// TODO in v2, allow the entire object to be passed in
+				validator.error(computation.value, {
+					code: `invalid-computed-argument`,
+					message: `Computed property argument must be a destructured object pattern`
+				});
+			}
+		} else {
+			params.forEach((param: Node) => {
+				const valid =
+					param.type === 'Identifier' ||
+					(param.type === 'AssignmentPattern' &&
+						param.left.type === 'Identifier');
+
+				if (!valid) {
+					// TODO change this for v2
+					validator.error(param, {
+						code: `invalid-computed-arguments`,
+						message: `Computed properties cannot use destructuring in function parameters`
+					});
+				}
+			});
+		}
 	});
 }


Test patch:
diff --git a/test/js/samples/dev-warning-missing-data-computed/_actual-bundle-v2.js b/test/js/samples/dev-warning-missing-data-computed/_actual-bundle-v2.js
--- a/test/js/samples/dev-warning-missing-data-computed/_actual-bundle-v2.js
+++ b/test/js/samples/dev-warning-missing-data-computed/_actual-bundle-v2.js
@@ -226,7 +226,7 @@ var protoDev = {
 
 /* generated by Svelte vX.Y.Z */
 
-function bar(foo) {
+function bar({ foo }) {
 	return foo * 2;
 }
 
@@ -291,7 +291,7 @@ SvelteComponent.prototype._checkReadOnly = function _checkReadOnly(newState) {
 
 SvelteComponent.prototype._recompute = function _recompute(changed, state) {
 	if (changed.foo) {
-		if (this._differs(state.bar, (state.bar = bar(state.foo)))) changed.bar = true;
+		if (this._differs(state.bar, (state.bar = bar(state)))) changed.bar = true;
 	}
 };
 
diff --git a/test/js/samples/dev-warning-missing-data-computed/expected-bundle-v2.js b/test/js/samples/dev-warning-missing-data-computed/expected-bundle-v2.js
--- a/test/js/samples/dev-warning-missing-data-computed/expected-bundle-v2.js
+++ b/test/js/samples/dev-warning-missing-data-computed/expected-bundle-v2.js
@@ -226,7 +226,7 @@ var protoDev = {
 
 /* generated by Svelte vX.Y.Z */
 
-function bar(foo) {
+function bar({ foo }) {
 	return foo * 2;
 }
 
@@ -291,7 +291,7 @@ SvelteComponent.prototype._checkReadOnly = function _checkReadOnly(newState) {
 
 SvelteComponent.prototype._recompute = function _recompute(changed, state) {
 	if (changed.foo) {
-		if (this._differs(state.bar, (state.bar = bar(state.foo)))) changed.bar = true;
+		if (this._differs(state.bar, (state.bar = bar(state)))) changed.bar = true;
 	}
 };
 
diff --git a/test/js/samples/dev-warning-missing-data-computed/expected-v2.js b/test/js/samples/dev-warning-missing-data-computed/expected-v2.js
--- a/test/js/samples/dev-warning-missing-data-computed/expected-v2.js
+++ b/test/js/samples/dev-warning-missing-data-computed/expected-v2.js
@@ -1,7 +1,7 @@
 /* generated by Svelte vX.Y.Z */
 import { appendNode, assign, createElement, createText, detachNode, init, insertNode, noop, protoDev } from "svelte/shared.js";
 
-function bar(foo) {
+function bar({ foo }) {
 	return foo * 2;
 }
 
@@ -66,7 +66,7 @@ SvelteComponent.prototype._checkReadOnly = function _checkReadOnly(newState) {
 
 SvelteComponent.prototype._recompute = function _recompute(changed, state) {
 	if (changed.foo) {
-		if (this._differs(state.bar, (state.bar = bar(state.foo)))) changed.bar = true;
+		if (this._differs(state.bar, (state.bar = bar(state)))) changed.bar = true;
 	}
 }
 export default SvelteComponent;
\ No newline at end of file
diff --git a/test/js/samples/dev-warning-missing-data-computed/input-v2.html b/test/js/samples/dev-warning-missing-data-computed/input-v2.html
--- a/test/js/samples/dev-warning-missing-data-computed/input-v2.html
+++ b/test/js/samples/dev-warning-missing-data-computed/input-v2.html
@@ -6,7 +6,7 @@
 <script>
 	export default {
 		computed: {
-			bar: foo => foo * 2
+			bar: ({ foo }) => foo * 2
 		}
 	};
 </script>
\ No newline at end of file
diff --git a/test/runtime/samples/computed-function/main-v2.html b/test/runtime/samples/computed-function/main-v2.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/computed-function/main-v2.html
@@ -0,0 +1,20 @@
+<p>{scale(x)}</p>
+
+<script>
+	export default {
+		data: () => ({
+			x: 5,
+			domain: [ 0, 10 ],
+			range: [ 0, 100 ]
+		}),
+
+		computed: {
+			scale: ({ domain, range }) => {
+				return num => {
+					const t = domain[0] + ( num - domain[0] ) / ( domain[1] - domain[0] );
+					return range[0] + t * ( range[1] - range[0] );
+				}
+			}
+		}
+	};
+</script>
diff --git a/test/runtime/samples/computed-values-deconflicted/main-v2.html b/test/runtime/samples/computed-values-deconflicted/main-v2.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/computed-values-deconflicted/main-v2.html
@@ -0,0 +1,14 @@
+<span>{state}</span>
+
+<script>
+	export default {
+		data() {
+			return {
+				x: 'waiting'
+			};
+		},
+		computed: {
+			state: ({ x }) => x
+		}
+	};
+</script>
\ No newline at end of file
diff --git a/test/runtime/samples/computed-values-default/main-v2.html b/test/runtime/samples/computed-values-default/main-v2.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/computed-values-default/main-v2.html
@@ -0,0 +1,9 @@
+<p>{foo}</p>
+
+<script>
+	export default {
+		computed: {
+			foo: ({ a = 1 }) => a * 2
+		}
+	};
+</script>
diff --git a/test/runtime/samples/computed-values-function-dependency/main-v2.html b/test/runtime/samples/computed-values-function-dependency/main-v2.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/computed-values-function-dependency/main-v2.html
@@ -0,0 +1,28 @@
+<p>{x}</p>
+
+<script>
+	let _x;
+
+	function getX () {
+		return _x;
+	}
+
+	export default {
+		data () {
+			return {
+				y: 1
+			};
+		},
+
+		computed: {
+			xGetter ({ y }) {
+				_x = y * 2;
+				return getX;
+			},
+
+			x ({ xGetter }) {
+				return xGetter();
+			}
+		}
+	};
+</script>
\ No newline at end of file
diff --git a/test/runtime/samples/computed-values/main-v2.html b/test/runtime/samples/computed-values/main-v2.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/computed-values/main-v2.html
@@ -0,0 +1,16 @@
+<p>{a} + {b} = {c}</p>
+<p>{c} * {c} = {cSquared}</p>
+
+<script>
+	export default {
+		data: () => ({
+			a: 1,
+			b: 2
+		}),
+
+		computed: {
+			c: ({ a, b }) => a + b,
+			cSquared: ({ c }) => c * c
+		}
+	};
+</script>