Repository: sveltejs/svelte (JavaScript)
Date: 2020-02-24 08:54:00+00:00
Task type: Bug Fix

Issue:
Unknown prop using export let and export function
Maybe a bug in this REPL: https://svelte.dev/repl/79aa242f7e6d4af684099868189d3cb4?version=3.18.0

If you click on button "toggle modal" in console you should have this warning:

`"<Form> was created with unknown prop 'saveForm'"`

If you remove from `Form.svelte`

- the line: `export let removeThis = null`
- and the line: `{removeThis}`

the warning disappear.

Am I wrong?




Issue discussion:
This looks like a bug, since `saveForm` is indeed declared, and `removeThis` should have no bearing on it. I'm going to mark it as such.

However, if you're trying to bind a variable through three layers of components like this I'd recommend using a store, since we're probably abusing bindings a bit using the current pattern, and that might cause unexpected results.
Somewhat related to https://github.com/sveltejs/svelte/issues/4403#issuecomment-585706838 .

Is it really right that just two layers of `bind:` might (reasonably be expected to) cause unexpected results?

Naively, I think a new user would naturally expect that if nested bind works at all then it should be documented and tested and supported as working - even if it does, say, get slow at high levels of nesting, or blow up on really ridiculously large levels.

Solution patch:
diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts
--- a/src/compiler/compile/render_dom/index.ts
+++ b/src/compiler/compile/render_dom/index.ts
@@ -259,6 +259,9 @@ export default function dom(
 		inject_state;
 	if (has_invalidate) {
 		args.push(x`$$props`, x`$$invalidate`);
+	} else if (component.compile_options.dev) {
+		// $$props arg is still needed for unknown prop check
+		args.push(x`$$props`);
 	}
 
 	const has_create_fragment = block.has_content();
@@ -300,6 +303,7 @@ export default function dom(
 	const initial_context = renderer.context.slice(0, i + 1);
 
 	const has_definition = (
+		component.compile_options.dev ||
 		(instance_javascript && instance_javascript.length > 0) ||
 		filtered_props.length > 0 ||
 		uses_props ||
@@ -379,7 +383,7 @@ export default function dom(
 		});
 
 		let unknown_props_check;
-		if (component.compile_options.dev && !component.var_lookup.has('$$props') && writable_props.length) {
+		if (component.compile_options.dev && !component.var_lookup.has('$$props')) {
 			unknown_props_check = b`
 				const writable_props = [${writable_props.map(prop => x`'${prop.export_name}'`)}];
 				@_Object.keys($$props).forEach(key => {


Test patch:
diff --git a/test/js/samples/debug-hoisted/expected.js b/test/js/samples/debug-hoisted/expected.js
--- a/test/js/samples/debug-hoisted/expected.js
+++ b/test/js/samples/debug-hoisted/expected.js
@@ -50,6 +50,12 @@ function create_fragment(ctx) {
 function instance($$self, $$props, $$invalidate) {
 	let obj = { x: 5 };
 	let kobzol = 5;
+	const writable_props = [];
+
+	Object.keys($$props).forEach(key => {
+		if (!~writable_props.indexOf(key) && key.slice(0, 2) !== "$$") console.warn(`<Component> was created with unknown prop '${key}'`);
+	});
+
 	$$self.$capture_state = () => ({ obj, kobzol });
 
 	$$self.$inject_state = $$props => {
diff --git a/test/js/samples/debug-no-dependencies/expected.js b/test/js/samples/debug-no-dependencies/expected.js
--- a/test/js/samples/debug-no-dependencies/expected.js
+++ b/test/js/samples/debug-no-dependencies/expected.js
@@ -134,10 +134,20 @@ function create_fragment(ctx) {
 	return block;
 }
 
+function instance($$self, $$props) {
+	const writable_props = [];
+
+	Object.keys($$props).forEach(key => {
+		if (!~writable_props.indexOf(key) && key.slice(0, 2) !== "$$") console.warn(`<Component> was created with unknown prop '${key}'`);
+	});
+
+	return [];
+}
+
 class Component extends SvelteComponentDev {
 	constructor(options) {
 		super(options);
-		init(this, options, null, create_fragment, safe_not_equal, {});
+		init(this, options, instance, create_fragment, safe_not_equal, {});
 
 		dispatch_dev("SvelteRegisterComponent", {
 			component: this,
diff --git a/test/js/samples/loop-protect/expected.js b/test/js/samples/loop-protect/expected.js
--- a/test/js/samples/loop-protect/expected.js
+++ b/test/js/samples/loop-protect/expected.js
@@ -6,6 +6,7 @@ import {
 	detach_dev,
 	dispatch_dev,
 	element,
+	globals,
 	init,
 	insert_dev,
 	loop_guard,
@@ -13,6 +14,7 @@ import {
 	safe_not_equal
 } from "svelte/internal";
 
+const { console: console_1 } = globals;
 const file = undefined;
 
 function create_fragment(ctx) {
@@ -102,6 +104,12 @@ function instance($$self, $$props, $$invalidate) {
 		} while (true);
 	}
 
+	const writable_props = [];
+
+	Object.keys($$props).forEach(key => {
+		if (!~writable_props.indexOf(key) && key.slice(0, 2) !== "$$") console_1.warn(`<Component> was created with unknown prop '${key}'`);
+	});
+
 	function div_binding($$value) {
 		binding_callbacks[$$value ? "unshift" : "push"](() => {
 			$$invalidate(0, node = $$value);
diff --git a/test/runtime/samples/dev-warning-unknown-props-2/Foo.svelte b/test/runtime/samples/dev-warning-unknown-props-2/Foo.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/dev-warning-unknown-props-2/Foo.svelte
@@ -0,0 +1 @@
+Foo
diff --git a/test/runtime/samples/dev-warning-unknown-props-2/_config.js b/test/runtime/samples/dev-warning-unknown-props-2/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/dev-warning-unknown-props-2/_config.js
@@ -0,0 +1,9 @@
+export default {
+	compileOptions: {
+		dev: true
+	},
+
+	warnings: [
+		`<Foo> was created with unknown prop 'fo'`
+	]
+};
diff --git a/test/runtime/samples/dev-warning-unknown-props-2/main.svelte b/test/runtime/samples/dev-warning-unknown-props-2/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/dev-warning-unknown-props-2/main.svelte
@@ -0,0 +1,5 @@
+<script>
+	import Foo from './Foo.svelte';
+</script>
+
+<Foo fo="sho"/>