Skip to content

Commit c67a6c1

Browse files
authored
fix: change virtual code to provide correct type information for reactive statements (#230)
* fix: change virtual code to provide correct type information for reactive statements * Create famous-camels-battle.md * fix: parsing error * docs: fix * chore: refactor * test: add test cases * chore: refactor * Update .eslintrc.js
1 parent 7c20c0c commit c67a6c1

File tree

86 files changed

+37969
-3019
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+37969
-3019
lines changed

.changeset/famous-camels-battle.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte-eslint-parser": minor
3+
---
4+
5+
fix: change virtual code to provide correct type information for reactive statements

docs/internal-mechanism.md

+66-8
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,10 @@ Parse the following virtual script code as a script:
5252
function inputHandler () {
5353
// process
5454
}
55-
;
55+
;function $_render1(){
5656

57-
(inputHandler)as ((e:'input' extends keyof HTMLElementEventMap?HTMLElementEventMap['input']:CustomEvent<any>)=>void);
57+
(inputHandler) as ((e:'input' extends keyof HTMLElementEventMap ? HTMLElementEventMap['input'] : CustomEvent<any>) => void );
58+
}
5859
```
5960

6061
This gives the correct type information to the inputHandler when used with `on:input={inputHandler}`.
@@ -64,6 +65,8 @@ The script AST for the HTML template is then remapped to the template AST.
6465
You can check what happens to virtual scripts in the Online Demo.
6566
https://ota-meshi.github.io/svelte-eslint-parser/virtual-script-code/
6667

68+
See also [Scope Types](#scope-types) section.
69+
6770
### `scopeManager`
6871

6972
This parser returns a ScopeManager instance.
@@ -88,13 +91,13 @@ Parse the following virtual script code as a script:
8891
```ts
8992

9093
const array = [1, 2, 3]
91-
;
94+
;function $_render1(){
9295

93-
94-
95-
96-
97-
Array.from(array).forEach((e)=>{const ee = e * 2;(ee);});
96+
Array.from(array).forEach((e) => {
97+
const ee = e * 2;
98+
(ee);
99+
});
100+
}
98101
```
99102

100103
This ensures that the variable `e` defined by `{#each}` is correctly scoped only within `{#each}`.
@@ -111,3 +114,58 @@ You can also check the results [Online DEMO](https://ota-meshi.github.io/svelte-
111114
ESLint custom parsers that provide their own AST require `visitorKeys` to properly traverse the node.
112115

113116
See https://eslint.org/docs/latest/developer-guide/working-with-custom-parsers.
117+
118+
## Scope Types
119+
120+
TypeScript's type inference is pretty good, so parsing Svelte as-is gives some wrong type information.
121+
122+
e.g.
123+
124+
```ts
125+
export let foo: { bar: number } | null = null
126+
127+
$: console.log(foo && foo.bar);
128+
// ^ never type
129+
```
130+
131+
(You can see it on [TypeScript Online Playground](https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBAG2PAZhCAuOBvOAjAQyiwDsBXAWz2CjgF84AfOchBOAXhbLYFgAoAQBIsAYwgkAzhCQA6BBADmACjQQ4AMg1w1swlACUAbgFwz5i5YsB6a3AB6LYADcacGAE8wwAUA))
132+
133+
In the above code, foo in `$:` should be `object` or `null` in `*.svelte`, but TypeScript infers that it is `null` only.
134+
135+
To avoid this problem, the parser generates virtual code and traps statements within `$:` to function scope.
136+
Then restore it to have the correct AST and ScopeManager.
137+
138+
For example:
139+
140+
```svelte
141+
<script lang="ts">
142+
export let foo: { bar: number } | null = null
143+
144+
$: console.log(foo && foo.bar);
145+
146+
$: r = foo && foo.bar;
147+
148+
$: ({ bar: n } = foo || { bar: 42 });
149+
</script>
150+
151+
{foo && foo.bar}
152+
```
153+
154+
Parse the following virtual script code as a script:
155+
156+
```ts
157+
158+
export let foo: { bar: number } | null = null
159+
160+
$: function $_reactiveStatementScopeFunction1(){console.log(foo && foo.bar);}
161+
162+
$: let r = $_reactiveVariableScopeFunction2();
163+
function $_reactiveVariableScopeFunction2(){return foo && foo.bar;}
164+
165+
$: let { bar: n } = $_reactiveVariableScopeFunction3();
166+
function $_reactiveVariableScopeFunction3(){return foo || { bar: 42 };}
167+
;function $_render4(){
168+
169+
(foo && foo.bar);
170+
}
171+
```

explorer-v2/src/lib/AstExplorer.svelte

+26-3
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,38 @@
2424
2525
let jsonEditor, sourceEditor;
2626
27+
$: hasLangTs = /lang\s*=\s*(?:"ts"|ts|'ts'|"typescript"|typescript|'typescript')/u.test(
28+
svelteValue
29+
);
30+
31+
let tsParser = undefined;
32+
$: {
33+
if (hasLangTs && !tsParser) {
34+
import('@typescript-eslint/parser').then((parser) => {
35+
if (typeof window !== 'undefined') {
36+
if (!window.process) {
37+
window.process = {
38+
cwd: () => '',
39+
env: {}
40+
};
41+
}
42+
}
43+
tsParser = parser;
44+
});
45+
}
46+
}
47+
2748
$: {
28-
refresh(options, svelteValue);
49+
refresh(options, svelteValue, tsParser);
2950
}
3051
31-
function refresh(options, svelteValue) {
52+
function refresh(options, svelteValue, tsParser) {
3253
let ast;
3354
const start = Date.now();
3455
try {
35-
ast = svelteEslintParser.parseForESLint(svelteValue).ast;
56+
ast = svelteEslintParser.parseForESLint(svelteValue, {
57+
parser: { ts: tsParser, typescript: tsParser }
58+
}).ast;
3659
} catch (e) {
3760
ast = {
3861
message: e.message,

explorer-v2/src/lib/ESLintPlayground.svelte

+19-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,23 @@
3434
let time = '';
3535
let options = {};
3636
37+
$: hasLangTs = /lang\s*=\s*(?:"ts"|ts|'ts'|"typescript"|typescript|'typescript')/u.test(code);
38+
let tsParser = undefined;
39+
$: {
40+
if (hasLangTs && !tsParser) {
41+
import('@typescript-eslint/parser').then((parser) => {
42+
if (typeof window !== 'undefined') {
43+
if (!window.process) {
44+
window.process = {
45+
cwd: () => '',
46+
env: {}
47+
};
48+
}
49+
}
50+
tsParser = parser;
51+
});
52+
}
53+
}
3754
$: {
3855
options = useEslintPluginSvelte3 ? getEslintPluginSvelte3Options() : {};
3956
}
@@ -124,7 +141,8 @@
124141
parser: useEslintPluginSvelte3 ? undefined : 'svelte-eslint-parser',
125142
parserOptions: {
126143
ecmaVersion: 2020,
127-
sourceType: 'module'
144+
sourceType: 'module',
145+
parser: { ts: tsParser, typescript: tsParser }
128146
},
129147
rules,
130148
env: {

explorer-v2/src/lib/Header.svelte

+5-10
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@
1010
normalizedPathname === normalizedPath || normalizedPathname === `${baseUrl}${normalizedPath}`
1111
);
1212
}
13-
14-
// eslint-disable-next-line no-process-env -- ignore
15-
const dev = process.env.NODE_ENV !== 'production';
1613
</script>
1714

1815
<header class="header">
@@ -35,13 +32,11 @@
3532
sveltekit:prefetch
3633
href="{baseUrl}/scope">Scope</a
3734
>
38-
{#if dev || isActive($page.url.pathname, `/virtual-script-code`)}
39-
<a
40-
class="menu"
41-
class:active={isActive($page.url.pathname, `/virtual-script-code`)}
42-
href="{baseUrl}/virtual-script-code">Virtual Script Code</a
43-
>
44-
{/if}
35+
<a
36+
class="menu"
37+
class:active={isActive($page.url.pathname, `/virtual-script-code`)}
38+
href="{baseUrl}/virtual-script-code">Virtual Script Code</a
39+
>
4540
<div class="debug">
4641
$page.url.pathname: {$page.url.pathname}
4742
baseUrl: {baseUrl}

explorer-v2/src/lib/ScopeExplorer.svelte

+25-3
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,36 @@
2323
let time = '';
2424
2525
let jsonEditor, sourceEditor;
26+
27+
$: hasLangTs = /lang\s*=\s*(?:"ts"|ts|'ts'|"typescript"|typescript|'typescript')/u.test(
28+
svelteValue
29+
);
30+
let tsParser = undefined;
31+
$: {
32+
if (hasLangTs && !tsParser) {
33+
import('@typescript-eslint/parser').then((parser) => {
34+
if (typeof window !== 'undefined') {
35+
if (!window.process) {
36+
window.process = {
37+
cwd: () => '',
38+
env: {}
39+
};
40+
}
41+
}
42+
tsParser = parser;
43+
});
44+
}
45+
}
2646
$: {
27-
refresh(options, svelteValue);
47+
refresh(options, svelteValue, tsParser);
2848
}
29-
function refresh(options, svelteValue) {
49+
function refresh(options, svelteValue, tsParser) {
3050
let scopeManager;
3151
const start = Date.now();
3252
try {
33-
scopeManager = svelteEslintParser.parseForESLint(svelteValue).scopeManager;
53+
scopeManager = svelteEslintParser.parseForESLint(svelteValue, {
54+
parser: { ts: tsParser, typescript: tsParser }
55+
}).scopeManager;
3456
} catch (e) {
3557
scopeJson = {
3658
json: JSON.stringify({

0 commit comments

Comments
 (0)