Skip to content

Commit

Permalink
fix: change virtual code to provide correct type information for reac…
Browse files Browse the repository at this point in the history
…tive 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
  • Loading branch information
ota-meshi authored Oct 25, 2022
1 parent 7c20c0c commit c67a6c1
Show file tree
Hide file tree
Showing 86 changed files with 37,969 additions and 3,019 deletions.
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

0 comments on commit c67a6c1

Please sign in to comment.