Repository: sveltejs/language-tools
Pull request #1223 (2021-10-30 10:07:35)

(fix) handle binding assignment to $store
#1207


Patch:
diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/binding.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/binding.ts
index aa3b9be7..7d503444 100644
--- a/packages/svelte2tsx/src/htmlxtojsx/nodes/binding.ts
+++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/binding.ts
@@ -13,6 +13,11 @@ const oneWayBindingAttributes: Map<string, string> = new Map(
             ])
         )
 );
+/**
+ * List of all binding names that are transformed to sth like `binding = variable`.
+ * This applies to readonly bindings and the this binding.
+ */
+export const assignmentBindings = new Set([...oneWayBindingAttributes.keys(), 'this']);
 
 /**
  * Transform bind:xxx into something that conforms to JSX
@@ -57,8 +62,8 @@ export function handleBinding(
         const thisType = getInstanceTypeSimple(el, str);
 
         if (thisType) {
-            str.overwrite(attr.start, attr.expression.start, '{...__sveltets_1_empty((');
-            const instanceOfThisAssignment = ' = ' + surroundWithIgnoreComments(thisType) + '))}';
+            str.overwrite(attr.start, attr.expression.start, '{...__sveltets_1_empty(');
+            const instanceOfThisAssignment = ' = ' + surroundWithIgnoreComments(thisType) + ')}';
             str.overwrite(attr.expression.end, attr.end, instanceOfThisAssignment);
             return;
         }
diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/Stores.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/Stores.ts
index 06faee33..e9f46c3c 100644
--- a/packages/svelte2tsx/src/svelte2tsx/nodes/Stores.ts
+++ b/packages/svelte2tsx/src/svelte2tsx/nodes/Stores.ts
@@ -2,9 +2,11 @@ import MagicString from 'magic-string';
 import { Node } from 'estree-walker';
 import { ScopeStack, Scope } from '../utils/Scope';
 import { isObjectKey, isMember } from '../../utils/svelteAst';
+import { assignmentBindings } from '../../htmlxtojsx/nodes/binding';
 
 export function handleStore(node: Node, parent: Node, str: MagicString): void {
     const storename = node.name.slice(1);
+
     //handle assign to
     if (parent.type == 'AssignmentExpression' && parent.left == node && parent.operator == '=') {
         const dollar = str.original.indexOf('$', node.start);
@@ -13,6 +15,7 @@ export function handleStore(node: Node, parent: Node, str: MagicString): void {
         str.appendLeft(parent.end, ')');
         return;
     }
+
     // handle Assignment operators ($store +=, -=, *=, /=, %=, **=, etc.)
     // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Assignment
     const operators = ['+=', '-=', '*=', '/=', '%=', '**=', '<<=', '>>=', '>>>=', '&=', '^=', '|='];
@@ -31,6 +34,7 @@ export function handleStore(node: Node, parent: Node, str: MagicString): void {
         str.appendLeft(parent.end, ')');
         return;
     }
+
     // handle $store++, $store--, ++$store, --$store
     if (parent.type == 'UpdateExpression') {
         let simpleOperator;
@@ -55,10 +59,21 @@ export function handleStore(node: Node, parent: Node, str: MagicString): void {
         return;
     }
 
+    const dollar = str.original.indexOf('$', node.start);
+
+    // handle bindings which are transformed to assignments. These need special treatment because
+    // `(__sveltets_1_store_get(foo), foo$) = something` is syntactically invalid
+    // Therefore remove the outer commas. Note: This relies on the binding expression wrapping
+    // this statement with __sveltets_1_empty
+    if (parent.type === 'Binding' && assignmentBindings.has(parent.name)) {
+        str.overwrite(dollar, dollar + 1, '__sveltets_1_store_get(', { contentOnly: true });
+        str.prependLeft(node.end, `), $${storename}`);
+        return;
+    }
+
     // we change "$store" references into "(__sveltets_1_store_get(store), $store)"
     // - in order to get ts errors if store is not assignable to SvelteStore
     // - use $store variable defined above to get ts flow control
-    const dollar = str.original.indexOf('$', node.start);
     str.overwrite(dollar, dollar + 1, '(__sveltets_1_store_get(', { contentOnly: true });
     str.prependLeft(node.end, `), $${storename})`);
 }
diff --git a/packages/svelte2tsx/svelte-shims.d.ts b/packages/svelte2tsx/svelte-shims.d.ts
index 084a88b9..f179c52b 100644
--- a/packages/svelte2tsx/svelte-shims.d.ts
+++ b/packages/svelte2tsx/svelte-shims.d.ts
@@ -160,7 +160,7 @@ declare function __sveltets_1_with_any_event<Props = {}, Events = {}, Slots = {}
 
 declare function __sveltets_1_store_get<T = any>(store: SvelteStore<T>): T
 declare function __sveltets_1_any(dummy: any): any;
-declare function __sveltets_1_empty(dummy: any): {};
+declare function __sveltets_1_empty(...dummy: any[]): {};
 declare function __sveltets_1_componentType(): AConstructorTypeOf<Svelte2TsxComponent<any, any, any>>
 declare function __sveltets_1_invalidate<T>(getValue: () => T): T
 


Test patch:
diff --git a/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts b/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts
index c7a35a63..4f84e9dd 100644
--- a/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts
+++ b/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts
@@ -2021,4 +2021,80 @@ describe('DiagnosticsProvider', () => {
             }
         ]);
     });
+
+    it('diagnoses bindings with $store', async () => {
+        const { plugin, document } = setup('bind-to-$store.svelte');
+
+        const diagnostics = await plugin.getDiagnostics(document);
+        assert.deepStrictEqual(diagnostics, [
+            {
+                range: {
+                    start: {
+                        line: 19,
+                        character: 33
+                    },
+                    end: {
+                        line: 19,
+                        character: 34
+                    }
+                },
+                severity: 1,
+                source: 'ts',
+                message: "Type 'number' is not assignable to type 'boolean'.",
+                code: 2322,
+                tags: []
+            },
+            {
+                range: {
+                    start: {
+                        line: 20,
+                        character: 16
+                    },
+                    end: {
+                        line: 20,
+                        character: 20
+                    }
+                },
+                severity: 1,
+                source: 'ts',
+                message: "Type 'boolean' is not assignable to type 'number'.",
+                code: 2322,
+                tags: []
+            },
+            {
+                range: {
+                    start: {
+                        line: 21,
+                        character: 24
+                    },
+                    end: {
+                        line: 21,
+                        character: 41
+                    }
+                },
+                severity: 1,
+                source: 'ts',
+                message: "Type 'number' is not assignable to type 'boolean'.",
+                code: 2322,
+                tags: []
+            },
+            {
+                range: {
+                    start: {
+                        line: 22,
+                        character: 16
+                    },
+                    end: {
+                        line: 22,
+                        character: 20
+                    }
+                },
+                severity: 1,
+                source: 'ts',
+                message: "Type 'boolean' is not assignable to type 'number'.",
+                code: 2322,
+                tags: []
+            }
+        ]);
+    });
 });
diff --git a/packages/language-server/test/plugins/typescript/testfiles/diagnostics/bind-to-$store.svelte b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/bind-to-$store.svelte
new file mode 100644
index 00000000..2275ee68
--- /dev/null
+++ b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/bind-to-$store.svelte
@@ -0,0 +1,23 @@
+<script lang="ts">
+  import { writable } from 'svelte/store';
+  import { SvelteComponentTyped } from 'svelte';
+
+  const storeNr = writable(1);
+  const storeBool = writable(true);
+  const storeObjNr = writable({foo: 1});
+  const storeObjBool = writable({foo: true});
+
+  class Component extends SvelteComponentTyped<{prop: number}> {}
+</script>
+
+<!-- valid -->
+<div bind:offsetHeight={$storeNr} />
+<Component bind:prop={$storeNr} />
+<div bind:offsetHeight={$storeObjNr.foo} />
+<Component bind:prop={$storeObjNr.foo} />
+
+<!-- error -->
+<div bind:offsetHeight={$storeBool} />
+<Component bind:prop={$storeBool} />
+<div bind:offsetHeight={$storeObjBool.foo} />
+<Component bind:prop={$storeObjBool.foo} />
diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/binding-this-component/expected.jsx b/packages/svelte2tsx/test/htmlx2jsx/samples/binding-this-component/expected.jsx
index 7a8e490a..b50b5b0b 100644
--- a/packages/svelte2tsx/test/htmlx2jsx/samples/binding-this-component/expected.jsx
+++ b/packages/svelte2tsx/test/htmlx2jsx/samples/binding-this-component/expected.jsx
@@ -1,1 +1,1 @@
-<><Component type="radio" {...__sveltets_1_empty((element = /*Ωignore_startΩ*/new Component({target: __sveltets_1_any(''), props: __sveltets_1_any('')})/*Ωignore_endΩ*/))} value="Plain"/></>
\ No newline at end of file
+<><Component type="radio" {...__sveltets_1_empty(element = /*Ωignore_startΩ*/new Component({target: __sveltets_1_any(''), props: __sveltets_1_any('')})/*Ωignore_endΩ*/)} value="Plain"/></>
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/binding-this-svelte-body/expected.jsx b/packages/svelte2tsx/test/htmlx2jsx/samples/binding-this-svelte-body/expected.jsx
index 2997455b..b3b8138b 100644
--- a/packages/svelte2tsx/test/htmlx2jsx/samples/binding-this-svelte-body/expected.jsx
+++ b/packages/svelte2tsx/test/htmlx2jsx/samples/binding-this-svelte-body/expected.jsx
@@ -1,1 +1,1 @@
-<><sveltebody {...__sveltets_1_empty((element = /*Ωignore_startΩ*/__sveltets_1_instanceOf(HTMLBodyElement)/*Ωignore_endΩ*/))} /></>
\ No newline at end of file
+<><sveltebody {...__sveltets_1_empty(element = /*Ωignore_startΩ*/__sveltets_1_instanceOf(HTMLBodyElement)/*Ωignore_endΩ*/)} /></>
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/binding-this-svelte-component/expected.jsx b/packages/svelte2tsx/test/htmlx2jsx/samples/binding-this-svelte-component/expected.jsx
index dbf6b4ff..b5bea26f 100644
--- a/packages/svelte2tsx/test/htmlx2jsx/samples/binding-this-svelte-component/expected.jsx
+++ b/packages/svelte2tsx/test/htmlx2jsx/samples/binding-this-svelte-component/expected.jsx
@@ -1,1 +1,1 @@
-<><sveltecomponent this={A} {...__sveltets_1_empty((element = /*Ωignore_startΩ*/new (A)({target: __sveltets_1_any(''), props: __sveltets_1_any('')})/*Ωignore_endΩ*/))} /></>
\ No newline at end of file
+<><sveltecomponent this={A} {...__sveltets_1_empty(element = /*Ωignore_startΩ*/new (A)({target: __sveltets_1_any(''), props: __sveltets_1_any('')})/*Ωignore_endΩ*/)} /></>
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/binding-this-svelte-self/expected.jsx b/packages/svelte2tsx/test/htmlx2jsx/samples/binding-this-svelte-self/expected.jsx
index e57f57e8..9e56b209 100644
--- a/packages/svelte2tsx/test/htmlx2jsx/samples/binding-this-svelte-self/expected.jsx
+++ b/packages/svelte2tsx/test/htmlx2jsx/samples/binding-this-svelte-self/expected.jsx
@@ -1,3 +1,3 @@
 <>{(false) ? <>
-    <svelteself {...__sveltets_1_empty((element = /*Ωignore_startΩ*/__sveltets_1_instanceOf(__sveltets_1_componentType())/*Ωignore_endΩ*/))} />
+    <svelteself {...__sveltets_1_empty(element = /*Ωignore_startΩ*/__sveltets_1_instanceOf(__sveltets_1_componentType())/*Ωignore_endΩ*/)} />
 </> : <></>}</>
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/binding-this/expected.jsx b/packages/svelte2tsx/test/htmlx2jsx/samples/binding-this/expected.jsx
index 0aa08856..cdd5bb8f 100644
--- a/packages/svelte2tsx/test/htmlx2jsx/samples/binding-this/expected.jsx
+++ b/packages/svelte2tsx/test/htmlx2jsx/samples/binding-this/expected.jsx
@@ -1,1 +1,1 @@
-<><input type="radio" {...__sveltets_1_empty((element = /*Ωignore_startΩ*/__sveltets_1_instanceOf(__sveltets_1_ctorOf(__sveltets_1_mapElementTag('input')))/*Ωignore_endΩ*/))} value="Plain"/></>
\ No newline at end of file
+<><input type="radio" {...__sveltets_1_empty(element = /*Ωignore_startΩ*/__sveltets_1_instanceOf(__sveltets_1_ctorOf(__sveltets_1_mapElementTag('input')))/*Ωignore_endΩ*/)} value="Plain"/></>
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/sourcemaps/samples/component-props/mappings.jsx b/packages/svelte2tsx/test/sourcemaps/samples/component-props/mappings.jsx
index 953be853..f1a7caec 100644
--- a/packages/svelte2tsx/test/sourcemaps/samples/component-props/mappings.jsx
+++ b/packages/svelte2tsx/test/sourcemaps/samples/component-props/mappings.jsx
@@ -27,11 +27,11 @@
 
 <Component                                                                                                                                            {/**
 ------------------------------------------------------------------------------------------------------------------------------------------------------ */}
-    {...__sveltets_1_empty((bar = /*Ωignore_startΩ*/new Component({target: __sveltets_1_any(''), props: __sveltets_1_any('')})/*Ωignore_endΩ*/))}     {/**
-   ╚{...__sveltets_1_empty((bar•=•/*Ωignore_startΩ*/new•Component({target:•__sveltets_1_any(''),•props:•__sveltets_1_any('')})/*Ωignore_endΩ*/))}↲    [generated] line 18
-   ╚b                       bar}                                                                                                                 ↲    
+    {...__sveltets_1_empty(bar = /*Ωignore_startΩ*/new Component({target: __sveltets_1_any(''), props: __sveltets_1_any('')})/*Ωignore_endΩ*/)}       {/**
+   ╚{...__sveltets_1_empty(bar•=•/*Ωignore_startΩ*/new•Component({target:•__sveltets_1_any(''),•props:•__sveltets_1_any('')})/*Ωignore_endΩ*/)}↲    [generated] line 18
+   ╚b                      bar}                                                                                                                ↲      
    ╚b          bar}↲                                                                                                                                  
-   ╚bind:this={bar}↲                                                                                                                                  [original] line 16 
+   ╚bind:this={bar}↲                                                                                                                                [original] line 16 
 ------------------------------------------------------------------------------------------------------------------------------------------------------ */}
 /></>                                                                                                                                                 {/**
 /></>↲    [generated] line 19                                                                                                                         
diff --git a/packages/svelte2tsx/test/sourcemaps/samples/large-sample-1/mappings.jsx b/packages/svelte2tsx/test/sourcemaps/samples/large-sample-1/mappings.jsx
index 63a5658c..3ca70f65 100644
--- a/packages/svelte2tsx/test/sourcemaps/samples/large-sample-1/mappings.jsx
+++ b/packages/svelte2tsx/test/sourcemaps/samples/large-sample-1/mappings.jsx
@@ -323,11 +323,11 @@ s
             </div>
                                                                                                                                                       {/**
 ------------------------------------------------------------------------------------------------------------------------------------------------------ */}
-            <div class="chapter-markup" {...__sveltets_1_empty((scrollable = /*Ωignore_startΩ*/__sveltets_1_instanceOf(__sveltets_1_ctorOf(__sveltets_1_mapElementTag('div')))/*Ωignore_endΩ*/))}>{/**
-         ╚╚╚<div•class="chapter-markup"•{...__sveltets_1_empty((scrollable•=•/*Ωignore_startΩ*/__sveltets_1_instanceOf(__sveltets_1_ctorOf(__sveltets_1_mapElementTag('div')))/*Ωignore_endΩ*/))}>↲    [generated] line 139
-         ╚╚╚<div•c    ="chapter-markup"•b                       scrollable}                                                                                                                      >↲    
-         ╚╚╚<div•c    ="chapter-markup"•b          scrollable}>↲                                                                                                                                       
-         ╚╚╚<div•class="chapter-markup"•bind:this={scrollable}>↲                                                                                                                                       [original] line 278 
+            <div class="chapter-markup" {...__sveltets_1_empty(scrollable = /*Ωignore_startΩ*/__sveltets_1_instanceOf(__sveltets_1_ctorOf(__sveltets_1_mapElementTag('div')))/*Ωignore_endΩ*/)}>{/**
+         ╚╚╚<div•class="chapter-markup"•{...__sveltets_1_empty(scrollable•=•/*Ωignore_startΩ*/__sveltets_1_instanceOf(__sveltets_1_ctorOf(__sveltets_1_mapElementTag('div')))/*Ωignore_endΩ*/)}>↲    [generated] line 139
+         ╚╚╚<div•c    ="chapter-markup"•b                      scrollable}                                                                                                                     >↲    
+         ╚╚╚<div•c    ="chapter-markup"•b          scrollable}>↲                                                                                                                                     
+         ╚╚╚<div•class="chapter-markup"•bind:this={scrollable}>↲                                                                                                                                     [original] line 278 
 ------------------------------------------------------------------------------------------------------------------------------------------------------ */}
                 { chapter.html}                                                                                                                       {/**
             ╚╚╚╚{•chapter.html}↲         [generated] line 140                                                                                         
@@ -400,11 +400,11 @@ s
         <div class="tutorial-repl">
             <Repl                                                                                                                                     {/**
 ------------------------------------------------------------------------------------------------------------------------------------------------------ */}
-                {...__sveltets_1_empty((repl = /*Ωignore_startΩ*/new Repl({target: __sveltets_1_any(''), props: __sveltets_1_any('')})/*Ωignore_endΩ*/))}{/**
-            ╚╚╚╚{...__sveltets_1_empty((repl•=•/*Ωignore_startΩ*/new•Repl({target:•__sveltets_1_any(''),•props:•__sveltets_1_any('')})/*Ωignore_endΩ*/))}↲    [generated] line 163
-            ╚╚╚╚b                       repl}                                                                                                            ↲    
-            ╚╚╚╚b          repl}↲                                                                                                                             
-            ╚╚╚╚bind:this={repl}↲                                                                                                                             [original] line 303 
+                {...__sveltets_1_empty(repl = /*Ωignore_startΩ*/new Repl({target: __sveltets_1_any(''), props: __sveltets_1_any('')})/*Ωignore_endΩ*/)}{/**
+            ╚╚╚╚{...__sveltets_1_empty(repl•=•/*Ωignore_startΩ*/new•Repl({target:•__sveltets_1_any(''),•props:•__sveltets_1_any('')})/*Ωignore_endΩ*/)}↲    [generated] line 163
+            ╚╚╚╚b                      repl}                                                                                                           ↲    
+            ╚╚╚╚b          repl}↲                                                                                                                           
+            ╚╚╚╚bind:this={repl}↲                                                                                                                           [original] line 303 
 ------------------------------------------------------------------------------------------------------------------------------------------------------ */}
                 workersUrl="workers"                                                                                                                  {/**
 ------------------------------------------------------------------------------------------------------------------------------------------------------ */}
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/binding-assignment-$store/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/binding-assignment-$store/expected.tsx
new file mode 100644
index 00000000..b02a5847
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/binding-assignment-$store/expected.tsx
@@ -0,0 +1,12 @@
+///<reference types="svelte" />
+<></>;function render() {
+<><div {...__sveltets_1_empty(__sveltets_1_store_get(compile_options), $compile_options=__sveltets_1_instanceOf(HTMLDivElement).offsetHeight)} />
+<div {...__sveltets_1_empty((__sveltets_1_store_get(compile_options), $compile_options).foo=__sveltets_1_instanceOf(HTMLDivElement).offsetHeight)} />
+<div {...__sveltets_1_empty(__sveltets_1_store_get(compile_options), $compile_options = /*Ωignore_startΩ*/__sveltets_1_instanceOf(__sveltets_1_ctorOf(__sveltets_1_mapElementTag('div')))/*Ωignore_endΩ*/)} />
+<div {...__sveltets_1_empty((__sveltets_1_store_get(compile_options), $compile_options).foo = /*Ωignore_startΩ*/__sveltets_1_instanceOf(__sveltets_1_ctorOf(__sveltets_1_mapElementTag('div')))/*Ωignore_endΩ*/)} />
+<div noAssignment={(__sveltets_1_store_get(compile_options), $compile_options)} />
+<div noAssignment={(__sveltets_1_store_get(compile_options), $compile_options).foo} /></>
+return { props: {}, slots: {}, getters: {}, events: {} }}
+
+export default class Input__SvelteComponent_ extends __sveltets_1_createSvelte2TsxComponent(__sveltets_1_partial(__sveltets_1_with_any_event(render()))) {
+}
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/binding-assignment-$store/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/binding-assignment-$store/input.svelte
new file mode 100644
index 00000000..b508059e
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/binding-assignment-$store/input.svelte
@@ -0,0 +1,6 @@
+<div bind:offsetHeight={$compile_options} />
+<div bind:offsetHeight={$compile_options.foo} />
+<div bind:this={$compile_options} />
+<div bind:this={$compile_options.foo} />
+<div bind:noAssignment={$compile_options} />
+<div bind:noAssignment={$compile_options.foo} />
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/slot-bind-this/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/slot-bind-this/expected.tsx
index 44933825..ff47d9a8 100644
--- a/packages/svelte2tsx/test/svelte2tsx/samples/slot-bind-this/expected.tsx
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/slot-bind-this/expected.tsx
@@ -1,7 +1,7 @@
 ///<reference types="svelte" />
 <></>;function render() {
 /*Ωignore_startΩ*/;const __sveltets_ensureSlot = __sveltets_1_createEnsureSlot();/*Ωignore_endΩ*/
-<><slot name="s" {...__sveltets_1_empty((s = /*Ωignore_startΩ*/__sveltets_1_instanceOf(HTMLSlotElement)/*Ωignore_endΩ*/))} /></>
+<><slot name="s" {...__sveltets_1_empty(s = /*Ωignore_startΩ*/__sveltets_1_instanceOf(HTMLSlotElement)/*Ωignore_endΩ*/)} /></>
 return { props: {}, slots: {'s': {}}, getters: {}, events: {} }}
 
 export default class Input__SvelteComponent_ extends __sveltets_1_createSvelte2TsxComponent(__sveltets_1_partial(__sveltets_1_with_any_event(render()))) {