Skip to content

Commit 5efe1c2

Browse files
ota-meshiJounQin
andauthored
Add docs for internal mechanism, and demo for debug (#179)
* Add docs for internal mechanism, and demo for debug * update * update * Update docs/internal-mechanism.md Co-authored-by: JounQin <[email protected]> * Update docs/internal-mechanism.md Co-authored-by: JounQin <[email protected]> * update Co-authored-by: JounQin <[email protected]>
1 parent 468f33c commit 5efe1c2

File tree

16 files changed

+283
-7
lines changed

16 files changed

+283
-7
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ Welcome contributing!
194194

195195
Please use GitHub's Issues/PRs.
196196

197+
See also the documentation for the internal mechanism.
198+
199+
- [internal-mechanism.md](./docs/internal-mechanism.md)
200+
197201
## :lock: License
198202

199203
See the [LICENSE](LICENSE) file for license rights and limitations (MIT).

docs/AST.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,8 @@ interface SvelteHTMLComment extends Node {
550550

551551
### SvelteReactiveStatement
552552

553-
This node is a reactive statement labeled with `$`.
553+
This node is a reactive statement labeled with `$`.
554+
`SvelteReactiveStatement` is a special node to avoid confusing ESLint check rules with `LabeledStatement`.
554555

555556
```ts
556557
interface SvelteReactiveStatement extends Node {

docs/internal-mechanism.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# What the Parser does
2+
3+
The main thing this parser does is parsing the `*.svelte` file and return an AST that can be parsed by ESLint.
4+
However, this parser does a few other things for a better experience with ESLint integration.
5+
6+
## Entry Point
7+
8+
This parser parses `*.svelte` via `parseForESLint()` and returns the result to ESLint. This is a requirement for ESLint's custom parser.
9+
10+
See https://eslint.org/docs/latest/developer-guide/working-with-custom-parsers.
11+
12+
## Results
13+
14+
### `ast`
15+
16+
Returns the AST of the parses result.
17+
18+
Script AST is [ESTree] compliant AST by default. However, if you used `@typescript-eslint/parser` as script parser, it may contain [TypeScript AST](https://github.com/typescript-eslint/typescript-eslint/tree/main/packages/ast-spec).
19+
However, the parser assigns a special node `SvelteReactiveStatement` to the parsed result of `$:`.
20+
`SvelteReactiveStatement` is a special node to avoid confusing ESLint check rules with `LabeledStatement`.
21+
22+
[ESTree]: https://github.com/estree/estree
23+
24+
The HTML template part returns a special AST. See [AST.md](./AST.md).
25+
26+
The `Program` node contains `tokens` and `comments`. This is a requirement for ESLint's custom parser.
27+
28+
See https://eslint.org/docs/latest/developer-guide/working-with-custom-parsers#the-ast-specification.
29+
30+
### `services`
31+
32+
This parser returns the `services` returned by the script parser.
33+
In particular, typescript-eslint contains important information such as type information in `services`. The parser does not edit the `services`, but there is a trick in parsing the script to get the correct result of the `services` returned by the script parser.
34+
35+
When parsing the script, the parser does not pass only the `<script>` part, but generates virtual script code including the script that converted the HTML template into a script, and lets the script parser parse it.
36+
37+
For example, if you enter `*.svelte` template to listen for input events:
38+
39+
```svelte
40+
<script lang="ts">
41+
function inputHandler () {
42+
// process
43+
}
44+
</script>
45+
<input on:input={inputHandler}>
46+
```
47+
48+
Parse the following virtual script code as a script:
49+
50+
```ts
51+
52+
function inputHandler () {
53+
// process
54+
}
55+
;
56+
57+
(inputHandler)as ((e:'input' extends keyof HTMLElementEventMap?HTMLElementEventMap['input']:CustomEvent<any>)=>void);
58+
```
59+
60+
This gives the correct type information to the inputHandler when used with `on:input={inputHandler}`.
61+
62+
The script AST for the HTML template is then remapped to the template AST.
63+
64+
You can check what happens to virtual scripts in the Online Demo.
65+
https://ota-meshi.github.io/svelte-eslint-parser/virtual-script-code/
66+
67+
### `scopeManager`
68+
69+
This parser returns a ScopeManager instance.
70+
ScopeManager is used in variable analysis such as [no-unused-vars](https://eslint.org/docs/latest/rules/no-unused-vars) and [no-undef](https://eslint.org/docs/latest/rules/no-undef) rules.
71+
See https://eslint.org/docs/latest/developer-guide/scope-manager-interface for details.
72+
73+
The parser will generate a virtual script so that it can parse the correct scope.
74+
For example, when using `{#each}` and `{@const}`:
75+
76+
```svelte
77+
<script lang="ts">
78+
const array = [1, 2, 3]
79+
</script>
80+
{#each array as e}
81+
{@const ee = e * 2}
82+
{ee}
83+
{/each}
84+
```
85+
86+
Parse the following virtual script code as a script:
87+
88+
```ts
89+
90+
const array = [1, 2, 3]
91+
;
92+
93+
94+
95+
96+
97+
Array.from(array).forEach((e)=>{const ee = e * 2;(ee);});
98+
```
99+
100+
This ensures that the variable `e` defined by `{#each}` is correctly scoped only within `{#each}`.
101+
102+
Also, this parser returns special results for variables used in `$: foo = expression` and `$count` for proper analysis.
103+
104+
It also adds virtual references for variables that are marked specially used in `*.svelte` (e.g. `export let` and `$ref`). This is a hack that is also used in typescript-eslint.
105+
https://github.com/typescript-eslint/typescript-eslint/issues/4508#issuecomment-1030508403
106+
107+
You can also check the results [Online DEMO](https://ota-meshi.github.io/svelte-eslint-parser/).
108+
109+
### `visitorKeys`
110+
111+
ESLint custom parsers that provide their own AST require `visitorKeys` to properly traverse the node.
112+
113+
See https://eslint.org/docs/latest/developer-guide/working-with-custom-parsers.

explorer-v2/build-system/pre-build/webpack.config.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,19 @@ export default [
7474
'svelte/compiler': '$$inject_svelte_compiler$$',
7575
espree: '$$inject_espree$$'
7676
},
77+
module: {
78+
rules: [
79+
{
80+
test: /\/resolve-parser\.js$/u,
81+
loader: 'string-replace-loader',
82+
options: {
83+
search: 'require\\(name\\)',
84+
replace: `__non_webpack_require__(name)`,
85+
flags: ''
86+
}
87+
}
88+
]
89+
},
7790
plugins: [
7891
new WrapperPlugin({
7992
test: /svelte-eslint-parser\.js/,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default {};

explorer-v2/build-system/shim/path.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
export default {
2-
extname
2+
extname,
3+
isAbsolute,
4+
join
35
};
46

57
export function extname(p) {
68
return /\.[^.]*$/.exec(p)?.[0];
79
}
10+
11+
export function isAbsolute() {
12+
return false;
13+
}
14+
export function join(...args) {
15+
return args.join('/');
16+
}

explorer-v2/build-system/shim/util.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ export default new Proxy(
22
{},
33
{
44
get(target, key) {
5-
console.log(key);
65
if (key === 'inspect') {
76
return {};
87
}
8+
// eslint-disable-next-line no-console -- Demo
9+
console.log(key);
910
return target[key];
1011
}
1112
}

explorer-v2/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313
},
1414
"dependencies": {
1515
"@fontsource/fira-mono": "^4.2.2",
16+
"@typescript-eslint/parser": "^5.30.3",
1617
"eslint": "^8.0.0",
1718
"eslint-plugin-svelte3": "^4.0.0",
1819
"eslint-scope": "^7.0.0",
1920
"pako": "^2.0.3",
2021
"svelte": "^3.41.0",
21-
"svelte-eslint-parser": "file:.."
22+
"svelte-eslint-parser": "link:.."
2223
},
2324
"devDependencies": {
2425
"@sveltejs/adapter-static": "^1.0.0-next.13",

explorer-v2/src/lib/Header.svelte

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,15 @@
44
import { base as baseUrl } from '$app/paths';
55
66
function isActive(pathname, path) {
7-
return pathname === path || pathname === `${baseUrl}${path}`;
7+
const normalizedPathname = pathname.replace(/\/$/u, '');
8+
const normalizedPath = path.replace(/\/$/u, '');
9+
return (
10+
normalizedPathname === normalizedPath || normalizedPathname === `${baseUrl}${normalizedPath}`
11+
);
812
}
13+
14+
// eslint-disable-next-line no-process-env -- ignore
15+
const dev = process.env.NODE_ENV !== 'production';
916
</script>
1017

1118
<header class="header">
@@ -28,6 +35,13 @@
2835
sveltekit:prefetch
2936
href="{baseUrl}/scope">Scope</a
3037
>
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}
3145
<div class="debug">
3246
$page.url.pathname: {$page.url.pathname}
3347
baseUrl: {baseUrl}

explorer-v2/src/lib/MonacoEditor.svelte

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
export let markers = [];
1717
export let rightMarkers = [];
1818
export let provideCodeActions = null;
19+
export let editorOptions = {};
1920
2021
export let waiting = null;
2122
let rootElement,
@@ -92,7 +93,8 @@
9293
renderIndentGuides: true,
9394
renderValidationDecorations: 'on',
9495
renderWhitespace: 'boundary',
95-
scrollBeyondLastLine: false
96+
scrollBeyondLastLine: false,
97+
...editorOptions
9698
};
9799
98100
if (diffEditor) {

0 commit comments

Comments
 (0)