Skip to content

fix: change virtual code to provide correct type information for reactive statements #230

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Oct 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/famous-camels-battle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte-eslint-parser": minor
---

fix: change virtual code to provide correct type information for reactive statements
74 changes: 66 additions & 8 deletions docs/internal-mechanism.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@ Parse the following virtual script code as a script:
function inputHandler () {
// process
}
;
;function $_render1(){

(inputHandler)as ((e:'input' extends keyof HTMLElementEventMap?HTMLElementEventMap['input']:CustomEvent<any>)=>void);
(inputHandler) as ((e:'input' extends keyof HTMLElementEventMap ? HTMLElementEventMap['input'] : CustomEvent<any>) => void );
}
```

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

See also [Scope Types](#scope-types) section.

### `scopeManager`

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

const array = [1, 2, 3]
;
;function $_render1(){


Array.from(array).forEach((e)=>{const ee = e * 2;(ee);});
Array.from(array).forEach((e) => {
const ee = e * 2;
(ee);
});
}
```

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

See https://eslint.org/docs/latest/developer-guide/working-with-custom-parsers.

## Scope Types

TypeScript's type inference is pretty good, so parsing Svelte as-is gives some wrong type information.

e.g.

```ts
export let foo: { bar: number } | null = null

$: console.log(foo && foo.bar);
// ^ never type
```

(You can see it on [TypeScript Online Playground](https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBAG2PAZhCAuOBvOAjAQyiwDsBXAWz2CjgF84AfOchBOAXhbLYFgAoAQBIsAYwgkAzhCQA6BBADmACjQQ4AMg1w1swlACUAbgFwz5i5YsB6a3AB6LYADcacGAE8wwAUA))

In the above code, foo in `$:` should be `object` or `null` in `*.svelte`, but TypeScript infers that it is `null` only.

To avoid this problem, the parser generates virtual code and traps statements within `$:` to function scope.
Then restore it to have the correct AST and ScopeManager.

For example:

```svelte
<script lang="ts">
export let foo: { bar: number } | null = null

$: console.log(foo && foo.bar);

$: r = foo && foo.bar;

$: ({ bar: n } = foo || { bar: 42 });
</script>

{foo && foo.bar}
```

Parse the following virtual script code as a script:

```ts

export let foo: { bar: number } | null = null

$: function $_reactiveStatementScopeFunction1(){console.log(foo && foo.bar);}

$: let r = $_reactiveVariableScopeFunction2();
function $_reactiveVariableScopeFunction2(){return foo && foo.bar;}

$: let { bar: n } = $_reactiveVariableScopeFunction3();
function $_reactiveVariableScopeFunction3(){return foo || { bar: 42 };}
;function $_render4(){

(foo && foo.bar);
}
```
29 changes: 26 additions & 3 deletions explorer-v2/src/lib/AstExplorer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,38 @@

let jsonEditor, sourceEditor;

$: hasLangTs = /lang\s*=\s*(?:"ts"|ts|'ts'|"typescript"|typescript|'typescript')/u.test(
svelteValue
);

let tsParser = undefined;
$: {
if (hasLangTs && !tsParser) {
import('@typescript-eslint/parser').then((parser) => {
if (typeof window !== 'undefined') {
if (!window.process) {
window.process = {
cwd: () => '',
env: {}
};
}
}
tsParser = parser;
});
}
}

$: {
refresh(options, svelteValue);
refresh(options, svelteValue, tsParser);
}

function refresh(options, svelteValue) {
function refresh(options, svelteValue, tsParser) {
let ast;
const start = Date.now();
try {
ast = svelteEslintParser.parseForESLint(svelteValue).ast;
ast = svelteEslintParser.parseForESLint(svelteValue, {
parser: { ts: tsParser, typescript: tsParser }
}).ast;
} catch (e) {
ast = {
message: e.message,
Expand Down
20 changes: 19 additions & 1 deletion explorer-v2/src/lib/ESLintPlayground.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,23 @@
let time = '';
let options = {};

$: hasLangTs = /lang\s*=\s*(?:"ts"|ts|'ts'|"typescript"|typescript|'typescript')/u.test(code);
let tsParser = undefined;
$: {
if (hasLangTs && !tsParser) {
import('@typescript-eslint/parser').then((parser) => {
if (typeof window !== 'undefined') {
if (!window.process) {
window.process = {
cwd: () => '',
env: {}
};
}
}
tsParser = parser;
});
}
}
$: {
options = useEslintPluginSvelte3 ? getEslintPluginSvelte3Options() : {};
}
Expand Down Expand Up @@ -124,7 +141,8 @@
parser: useEslintPluginSvelte3 ? undefined : 'svelte-eslint-parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module'
sourceType: 'module',
parser: { ts: tsParser, typescript: tsParser }
},
rules,
env: {
Expand Down
15 changes: 5 additions & 10 deletions explorer-v2/src/lib/Header.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@
normalizedPathname === normalizedPath || normalizedPathname === `${baseUrl}${normalizedPath}`
);
}

// eslint-disable-next-line no-process-env -- ignore
const dev = process.env.NODE_ENV !== 'production';
</script>

<header class="header">
Expand All @@ -35,13 +32,11 @@
sveltekit:prefetch
href="{baseUrl}/scope">Scope</a
>
{#if dev || isActive($page.url.pathname, `/virtual-script-code`)}
<a
class="menu"
class:active={isActive($page.url.pathname, `/virtual-script-code`)}
href="{baseUrl}/virtual-script-code">Virtual Script Code</a
>
{/if}
<a
class="menu"
class:active={isActive($page.url.pathname, `/virtual-script-code`)}
href="{baseUrl}/virtual-script-code">Virtual Script Code</a
>
<div class="debug">
$page.url.pathname: {$page.url.pathname}
baseUrl: {baseUrl}
Expand Down
28 changes: 25 additions & 3 deletions explorer-v2/src/lib/ScopeExplorer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,36 @@
let time = '';

let jsonEditor, sourceEditor;

$: hasLangTs = /lang\s*=\s*(?:"ts"|ts|'ts'|"typescript"|typescript|'typescript')/u.test(
svelteValue
);
let tsParser = undefined;
$: {
if (hasLangTs && !tsParser) {
import('@typescript-eslint/parser').then((parser) => {
if (typeof window !== 'undefined') {
if (!window.process) {
window.process = {
cwd: () => '',
env: {}
};
}
}
tsParser = parser;
});
}
}
$: {
refresh(options, svelteValue);
refresh(options, svelteValue, tsParser);
}
function refresh(options, svelteValue) {
function refresh(options, svelteValue, tsParser) {
let scopeManager;
const start = Date.now();
try {
scopeManager = svelteEslintParser.parseForESLint(svelteValue).scopeManager;
scopeManager = svelteEslintParser.parseForESLint(svelteValue, {
parser: { ts: tsParser, typescript: tsParser }
}).scopeManager;
} catch (e) {
scopeJson = {
json: JSON.stringify({
Expand Down
Loading