Repository: sveltejs/language-tools
Pull request #962 (2021-04-23 15:27:28)

(fix) svelte2tsx string literal transformation
The value of a string literal is the value without quotes, so we need to special-case that when generating the component instance
#961


Patch:
diff --git a/packages/language-server/src/lib/documents/Document.ts b/packages/language-server/src/lib/documents/Document.ts
index a0300011..3272530f 100644
--- a/packages/language-server/src/lib/documents/Document.ts
+++ b/packages/language-server/src/lib/documents/Document.ts
@@ -1,6 +1,6 @@
 import { urlToPath } from '../../utils';
 import { WritableDocument } from './DocumentBase';
-import { extractScriptTags, extractStyleTag, TagInformation } from './utils';
+import { extractScriptTags, extractStyleTag, extractTemplateTag, TagInformation } from './utils';
 import { parseHtml } from './parseHtml';
 import { SvelteConfig, configLoader } from './configLoader';
 import { HTMLDocument } from 'vscode-html-languageservice';
@@ -13,6 +13,7 @@ export class Document extends WritableDocument {
     scriptInfo: TagInformation | null = null;
     moduleScriptInfo: TagInformation | null = null;
     styleInfo: TagInformation | null = null;
+    templateInfo: TagInformation | null = null;
     configPromise: Promise<SvelteConfig | undefined>;
     config?: SvelteConfig;
     html!: HTMLDocument;
@@ -25,8 +26,8 @@ export class Document extends WritableDocument {
 
     private updateDocInfo() {
         this.html = parseHtml(this.content);
-        const scriptTags = extractScriptTags(this.content, this.html);
         const update = (config: SvelteConfig | undefined) => {
+            const scriptTags = extractScriptTags(this.content, this.html);
             this.config = config;
             this.scriptInfo = this.addDefaultLanguage(config, scriptTags?.script || null, 'script');
             this.moduleScriptInfo = this.addDefaultLanguage(
@@ -39,6 +40,11 @@ export class Document extends WritableDocument {
                 extractStyleTag(this.content, this.html),
                 'style'
             );
+            this.templateInfo = this.addDefaultLanguage(
+                config,
+                extractTemplateTag(this.content, this.html),
+                'markup'
+            );
         };
 
         const config = configLoader.getConfig(this.getFilePath() || '');
@@ -82,14 +88,16 @@ export class Document extends WritableDocument {
     }
 
     /**
-     * Returns the language associated to either script or style.
+     * Returns the language associated to script, style or template.
      * Returns an empty string if there's nothing set.
      */
-    getLanguageAttribute(tag: 'script' | 'style'): string {
+    getLanguageAttribute(tag: 'script' | 'style' | 'template'): string {
         const attrs =
             (tag === 'style'
                 ? this.styleInfo?.attributes
-                : this.scriptInfo?.attributes || this.moduleScriptInfo?.attributes) || {};
+                : tag === 'script'
+                ? this.scriptInfo?.attributes || this.moduleScriptInfo?.attributes
+                : this.templateInfo?.attributes) || {};
         const lang = attrs.lang || attrs.type || '';
         return lang.replace(/^text\//, '');
     }
@@ -97,7 +105,7 @@ export class Document extends WritableDocument {
     private addDefaultLanguage(
         config: SvelteConfig | undefined,
         tagInfo: TagInformation | null,
-        tag: 'style' | 'script'
+        tag: 'style' | 'script' | 'markup'
     ): TagInformation | null {
         if (!tagInfo || !config) {
             return tagInfo;
diff --git a/packages/language-server/src/lib/documents/utils.ts b/packages/language-server/src/lib/documents/utils.ts
index f0553af5..30a8c186 100644
--- a/packages/language-server/src/lib/documents/utils.ts
+++ b/packages/language-server/src/lib/documents/utils.ts
@@ -72,7 +72,11 @@ function blankIfBlocks(text: string): string {
  * @param source text content to extract tag from
  * @param tag the tag to extract
  */
-function extractTags(text: string, tag: 'script' | 'style', html?: HTMLDocument): TagInformation[] {
+function extractTags(
+    text: string,
+    tag: 'script' | 'style' | 'template',
+    html?: HTMLDocument
+): TagInformation[] {
     text = blankIfBlocks(text);
     const rootNodes = html?.roots || parseHtml(text).roots;
     const matchedNodes = rootNodes
@@ -173,6 +177,16 @@ export function extractStyleTag(source: string, html?: HTMLDocument): TagInforma
     return styles[0];
 }
 
+export function extractTemplateTag(source: string, html?: HTMLDocument): TagInformation | null {
+    const templates = extractTags(source, 'template', html);
+    if (!templates.length) {
+        return null;
+    }
+
+    // There should only be one style tag
+    return templates[0];
+}
+
 /**
  * Get the line and character based on the offset
  * @param offset The index of the position
diff --git a/packages/language-server/src/plugins/typescript/features/DiagnosticsProvider.ts b/packages/language-server/src/plugins/typescript/features/DiagnosticsProvider.ts
index 33c0771f..e2272e9e 100644
--- a/packages/language-server/src/plugins/typescript/features/DiagnosticsProvider.ts
+++ b/packages/language-server/src/plugins/typescript/features/DiagnosticsProvider.ts
@@ -1,6 +1,11 @@
 import ts from 'typescript';
 import { Diagnostic, DiagnosticSeverity, DiagnosticTag } from 'vscode-languageserver';
-import { Document, mapObjWithRangeToOriginal, getTextInRange } from '../../../lib/documents';
+import {
+    Document,
+    mapObjWithRangeToOriginal,
+    getTextInRange,
+    isRangeInTag
+} from '../../../lib/documents';
 import { DiagnosticsProvider } from '../../interfaces';
 import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
 import { convertRange, mapSeverity } from '../utils';
@@ -52,7 +57,7 @@ export class DiagnosticsProviderImpl implements DiagnosticsProvider {
             }))
             .map((diagnostic) => mapObjWithRangeToOriginal(fragment, diagnostic))
             .filter(hasNoNegativeLines)
-            .filter(isNoFalsePositive(document.getText(), tsDoc))
+            .filter(isNoFalsePositive(document, tsDoc))
             .map(enhanceIfNecessary)
             .map(swapRangeStartEndIfNecessary);
     }
@@ -82,16 +87,32 @@ function hasNoNegativeLines(diagnostic: Diagnostic): boolean {
     return diagnostic.range.start.line >= 0 && diagnostic.range.end.line >= 0;
 }
 
-function isNoFalsePositive(text: string, tsDoc: SvelteDocumentSnapshot) {
-    return (diagnostic: Diagnostic, idx: number) => {
+function isNoFalsePositive(document: Document, tsDoc: SvelteDocumentSnapshot) {
+    const text = document.getText();
+    const usesPug = document.getLanguageAttribute('template') === 'pug';
+
+    return (diagnostic: Diagnostic) => {
         return (
             isNoJsxCannotHaveMultipleAttrsError(diagnostic) &&
             isNoUnusedLabelWarningForReactiveStatement(diagnostic) &&
-            isNoUsedBeforeAssigned(diagnostic, text, tsDoc)
+            isNoUsedBeforeAssigned(diagnostic, text, tsDoc) &&
+            (!usesPug || isNoPugFalsePositive(diagnostic, document))
         );
     };
 }
 
+/**
+ * All diagnostics inside the template tag and the unused import/variable diagnostics
+ * are marked as false positive.
+ */
+function isNoPugFalsePositive(diagnostic: Diagnostic, document: Document): boolean {
+    return (
+        !isRangeInTag(diagnostic.range, document.templateInfo) &&
+        diagnostic.code !== 6133 &&
+        diagnostic.code !== 6192
+    );
+}
+
 /**
  * Variable used before being assigned, can happen when  you do `export let x`
  * without assigning a value in strict mode. Should not throw an error here
diff --git a/packages/svelte2tsx/src/htmlxtojsx/utils/node-utils.ts b/packages/svelte2tsx/src/htmlxtojsx/utils/node-utils.ts
index 26751ab3..bc9303a7 100644
--- a/packages/svelte2tsx/src/htmlxtojsx/utils/node-utils.ts
+++ b/packages/svelte2tsx/src/htmlxtojsx/utils/node-utils.ts
@@ -129,7 +129,9 @@ function getNameValuePairsFromAttributes(
                         return { name, value: valueStr, identifier: valueStr };
                     }
                     if (val.expression.type === 'Literal') {
-                        return { name, value: val.expression.value };
+                        const value =
+                            typeof val.expression.value === 'string' ? '""' : val.expression.value;
+                        return { name, value };
                     }
                     return { name, value: valueStr, complexExpression: true };
                 }


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 04ffb7ec..801cba91 100644
--- a/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts
+++ b/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts
@@ -871,4 +871,63 @@ describe('DiagnosticsProvider', () => {
             }
         ]);
     });
+
+    it('Pug: Ignores errors in template and unused warnings in script', async () => {
+        const { plugin, document } = setup('diagnostics-pug.svelte');
+        const diagnostics = await plugin.getDiagnostics(document);
+
+        assert.deepStrictEqual(diagnostics, [
+            {
+                code: 2307,
+                message: "Cannot find module '.' or its corresponding type declarations.",
+                range: {
+                    end: {
+                        character: 22,
+                        line: 1
+                    },
+                    start: {
+                        character: 19,
+                        line: 1
+                    }
+                },
+                severity: 1,
+                source: 'ts',
+                tags: []
+            },
+            {
+                code: 2307,
+                message: "Cannot find module '.' or its corresponding type declarations.",
+                range: {
+                    end: {
+                        character: 30,
+                        line: 2
+                    },
+                    start: {
+                        character: 27,
+                        line: 2
+                    }
+                },
+                severity: 1,
+                source: 'ts',
+                tags: []
+            },
+            {
+                code: 2322,
+                message: "Type 'boolean' is not assignable to type 'string | number'.",
+                range: {
+                    end: {
+                        character: 10,
+                        line: 4
+                    },
+                    start: {
+                        character: 9,
+                        line: 4
+                    }
+                },
+                severity: 1,
+                source: 'ts',
+                tags: []
+            }
+        ]);
+    });
 });
diff --git a/packages/language-server/test/plugins/typescript/testfiles/diagnostics/diagnostics-pug.svelte b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/diagnostics-pug.svelte
new file mode 100644
index 00000000..722fc469
--- /dev/null
+++ b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/diagnostics-pug.svelte
@@ -0,0 +1,11 @@
+<script lang="ts">
+   import Foo from '.';
+   import { A, B, C } from '.';
+
+   const a: string | number = true;
+</script>
+
+<template lang="pug">
++if('typeof a === "number"')
+  A(a='{a.toFixed(2)}')
+</template>
diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/component-slot-infer-props/expected.jsx b/packages/svelte2tsx/test/htmlx2jsx/samples/component-slot-infer-props/expected.jsx
index 42d72d9f..78ecaf64 100644
--- a/packages/svelte2tsx/test/htmlx2jsx/samples/component-slot-infer-props/expected.jsx
+++ b/packages/svelte2tsx/test/htmlx2jsx/samples/component-slot-infer-props/expected.jsx
@@ -1,5 +1,5 @@
-<><Parent bare shorthand={shorthand} text1="val1" text2="val2" text3={`a${a}b${b}`} textEmpty="" literal={true} complex={{a}} a-dashed-complex={{a}} >{() => {/*Ωignore_startΩ*/const Ψcomplex={a},Ψa_dashed_complex={a};/*Ωignore_endΩ*/() => { let {foo} = /*Ωignore_startΩ*/new Parent({target: __sveltets_any(''), props: {'bare':true, 'shorthand':shorthand, 'text1':"", 'text2':"", 'text3':"", 'textEmpty':"", 'literal':true, 'complex':Ψcomplex, 'a-dashed-complex':Ψa_dashed_complex}})/*Ωignore_endΩ*/.$$slot_def['default'];<>
-    {() => { let {bar} = /*Ωignore_startΩ*/new Parent({target: __sveltets_any(''), props: {'bare':true, 'shorthand':shorthand, 'text1':"", 'text2':"", 'text3':"", 'textEmpty':"", 'literal':true, 'complex':Ψcomplex, 'a-dashed-complex':Ψa_dashed_complex}})/*Ωignore_endΩ*/.$$slot_def['named'];<><Component  >
+<><Parent bare shorthand={shorthand} text1="val1" text2="val2" text3={`a${a}b${b}`} textEmpty="" literal={true} strLiteral={'foo'} complex={{a}} a-dashed-complex={{a}} >{() => {/*Ωignore_startΩ*/const Ψcomplex={a},Ψa_dashed_complex={a};/*Ωignore_endΩ*/() => { let {foo} = /*Ωignore_startΩ*/new Parent({target: __sveltets_any(''), props: {'bare':true, 'shorthand':shorthand, 'text1':"", 'text2':"", 'text3':"", 'textEmpty':"", 'literal':true, 'strLiteral':"", 'complex':Ψcomplex, 'a-dashed-complex':Ψa_dashed_complex}})/*Ωignore_endΩ*/.$$slot_def['default'];<>
+    {() => { let {bar} = /*Ωignore_startΩ*/new Parent({target: __sveltets_any(''), props: {'bare':true, 'shorthand':shorthand, 'text1':"", 'text2':"", 'text3':"", 'textEmpty':"", 'literal':true, 'strLiteral':"", 'complex':Ψcomplex, 'a-dashed-complex':Ψa_dashed_complex}})/*Ωignore_endΩ*/.$$slot_def['named'];<><Component  >
         {foo} {bar}
     </Component></>}}
     <div>
diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/component-slot-infer-props/input.svelte b/packages/svelte2tsx/test/htmlx2jsx/samples/component-slot-infer-props/input.svelte
index 310d50fd..bd71e0b3 100644
--- a/packages/svelte2tsx/test/htmlx2jsx/samples/component-slot-infer-props/input.svelte
+++ b/packages/svelte2tsx/test/htmlx2jsx/samples/component-slot-infer-props/input.svelte
@@ -1,4 +1,4 @@
-<Parent bare {shorthand} text1=val1 text2="val2" text3="a{a}b{b}" textEmpty="" literal={true} complex={{a}} a-dashed-complex={{a}} let:foo>
+<Parent bare {shorthand} text1=val1 text2="val2" text3="a{a}b{b}" textEmpty="" literal={true} strLiteral={'foo'} complex={{a}} a-dashed-complex={{a}} let:foo>
     <Component slot="named" let:bar>
         {foo} {bar}
     </Component>
diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/event-handler-component-infer-props/expected.jsx b/packages/svelte2tsx/test/htmlx2jsx/samples/event-handler-component-infer-props/expected.jsx
index 18f05d97..f66f68b7 100644
--- a/packages/svelte2tsx/test/htmlx2jsx/samples/event-handler-component-infer-props/expected.jsx
+++ b/packages/svelte2tsx/test/htmlx2jsx/samples/event-handler-component-infer-props/expected.jsx
@@ -1,1 +1,1 @@
-<><Component  bare shorthand={shorthand} text1="val1" text2="val2" text3={`a${a}b${b}`} textEmpty="" literal={true} complex={{a}} a-dashed-complex={{a}}  />{/*Ωignore_startΩ*/new Component({target: __sveltets_any(''), props: {'bare':true, 'shorthand':shorthand, 'text1':"", 'text2':"", 'text3':"", 'textEmpty':"", 'literal':true, 'complex':{a}, 'a-dashed-complex':{a}}})/*Ωignore_endΩ*/.$on('click', e => e)}</>
\ No newline at end of file
+<><Component bare shorthand={shorthand} text1="val1" text2="val2" text3={`a${a}b${b}`} textEmpty="" literal={true} strLiteral={'foo'} complex={{a}} a-dashed-complex={{a}}  />{/*Ωignore_startΩ*/new Component({target: __sveltets_any(''), props: {'bare':true, 'shorthand':shorthand, 'text1':"", 'text2':"", 'text3':"", 'textEmpty':"", 'literal':true, 'strLiteral':"", 'complex':{a}, 'a-dashed-complex':{a}}})/*Ωignore_endΩ*/.$on('click', e => e)}</>
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/event-handler-component-infer-props/input.svelte b/packages/svelte2tsx/test/htmlx2jsx/samples/event-handler-component-infer-props/input.svelte
index e666ae09..5ddbd664 100644
--- a/packages/svelte2tsx/test/htmlx2jsx/samples/event-handler-component-infer-props/input.svelte
+++ b/packages/svelte2tsx/test/htmlx2jsx/samples/event-handler-component-infer-props/input.svelte
@@ -1,1 +1,1 @@
-<Component  bare {shorthand} text1=val1 text2="val2" text3="a{a}b{b}" textEmpty="" literal={true} complex={{a}} a-dashed-complex={{a}} on:click={e => e} />
\ No newline at end of file
+<Component bare {shorthand} text1=val1 text2="val2" text3="a{a}b{b}" textEmpty="" literal={true} strLiteral={'foo'} complex={{a}} a-dashed-complex={{a}} on:click={e => e} />
\ No newline at end of file